Add support for donation dialog (fixes #4856) (#5318)

* Add support for donation dialog (fixes #4856)

* more changes

* test

* remove files

* default value for new user last_donation_notification

* move disable_donation_dialog to local_site

* restore formatting
This commit is contained in:
Nutomic 2025-01-17 12:28:41 +00:00 committed by GitHub
parent 4120c2fc2f
commit 67b36c6537
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 78 additions and 44 deletions

View file

@ -0,0 +1,19 @@
use actix_web::web::{Data, Json};
use chrono::Utc;
use lemmy_api_common::{context::LemmyContext, SuccessResponse};
use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult;
pub async fn donation_dialog_shown(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
let form = LocalUserUpdateForm {
last_donation_notification: Some(Utc::now()),
..Default::default()
};
LocalUser::update(&mut context.pool(), local_user_view.local_user.id, &form).await?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -3,6 +3,7 @@ pub mod ban_person;
pub mod block; pub mod block;
pub mod change_password; pub mod change_password;
pub mod change_password_after_reset; pub mod change_password_after_reset;
pub mod donation_dialog_shown;
pub mod generate_totp_secret; pub mod generate_totp_secret;
pub mod get_captcha; pub mod get_captcha;
pub mod list_banned; pub mod list_banned;

View file

@ -238,6 +238,8 @@ pub struct CreateSite {
pub comment_upvotes: Option<FederationMode>, pub comment_upvotes: Option<FederationMode>,
#[cfg_attr(feature = "full", ts(optional))] #[cfg_attr(feature = "full", ts(optional))]
pub comment_downvotes: Option<FederationMode>, pub comment_downvotes: Option<FederationMode>,
#[cfg_attr(feature = "full", ts(optional))]
pub disable_donation_dialog: Option<bool>,
} }
#[skip_serializing_none] #[skip_serializing_none]
@ -365,6 +367,10 @@ pub struct EditSite {
/// What kind of comment downvotes your site allows. /// What kind of comment downvotes your site allows.
#[cfg_attr(feature = "full", ts(optional))] #[cfg_attr(feature = "full", ts(optional))]
pub comment_downvotes: Option<FederationMode>, pub comment_downvotes: Option<FederationMode>,
/// If this is true, users will never see the dialog asking to support Lemmy development with
/// donations.
#[cfg_attr(feature = "full", ts(optional))]
pub disable_donation_dialog: Option<bool>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]

View file

@ -105,6 +105,7 @@ pub async fn create_site(
post_downvotes: data.post_downvotes, post_downvotes: data.post_downvotes,
comment_upvotes: data.comment_upvotes, comment_upvotes: data.comment_upvotes,
comment_downvotes: data.comment_downvotes, comment_downvotes: data.comment_downvotes,
disable_donation_dialog: data.disable_donation_dialog,
..Default::default() ..Default::default()
}; };

View file

@ -111,6 +111,7 @@ pub async fn update_site(
post_downvotes: data.post_downvotes, post_downvotes: data.post_downvotes,
comment_upvotes: data.comment_upvotes, comment_upvotes: data.comment_upvotes,
comment_downvotes: data.comment_downvotes, comment_downvotes: data.comment_downvotes,
disable_donation_dialog: data.disable_donation_dialog,
..Default::default() ..Default::default()
}; };

View file

@ -21,7 +21,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
aggregates::structs::PersonAggregates, aggregates::structs::PersonAggregates,
newtypes::{InstanceId, OAuthProviderId, SiteId}, newtypes::{InstanceId, OAuthProviderId},
source::{ source::{
actor_language::SiteLanguage, actor_language::SiteLanguage,
captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer}, captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer},
@ -139,21 +139,11 @@ pub async fn register(
email: data.email.as_deref().map(str::to_lowercase), email: data.email.as_deref().map(str::to_lowercase),
show_nsfw: Some(show_nsfw), show_nsfw: Some(show_nsfw),
accepted_application, accepted_application,
default_listing_type: Some(local_site.default_post_listing_type),
post_listing_mode: Some(local_site.default_post_listing_mode),
interface_language: language_tags.first().cloned(),
// If its the initial site setup, they are an admin
admin: Some(!local_site.site_setup),
..LocalUserInsertForm::new(inserted_person.id, Some(data.password.to_string())) ..LocalUserInsertForm::new(inserted_person.id, Some(data.password.to_string()))
}; };
let inserted_local_user = create_local_user( let inserted_local_user =
&context, create_local_user(&context, language_tags, local_user_form, &local_site).await?;
language_tags,
&local_user_form,
local_site.site_id,
)
.await?;
if local_site.site_setup && require_registration_application { if local_site.site_setup && require_registration_application {
if let Some(answer) = data.answer.clone() { if let Some(answer) = data.answer.clone() {
@ -369,20 +359,10 @@ pub async fn authenticate_with_oauth(
show_nsfw: Some(show_nsfw), show_nsfw: Some(show_nsfw),
accepted_application: Some(!require_registration_application), accepted_application: Some(!require_registration_application),
email_verified: Some(oauth_provider.auto_verify_email), email_verified: Some(oauth_provider.auto_verify_email),
post_listing_mode: Some(local_site.default_post_listing_mode),
interface_language: language_tags.first().cloned(),
// If its the initial site setup, they are an admin
admin: Some(!local_site.site_setup),
..LocalUserInsertForm::new(person.id, None) ..LocalUserInsertForm::new(person.id, None)
}; };
local_user = create_local_user( local_user = create_local_user(&context, language_tags, local_user_form, &local_site).await?;
&context,
language_tags,
&local_user_form,
local_site.site_id,
)
.await?;
// Create the oauth account // Create the oauth account
let oauth_account_form = let oauth_account_form =
@ -472,28 +452,33 @@ fn get_language_tags(req: &HttpRequest) -> Vec<String> {
async fn create_local_user( async fn create_local_user(
context: &Data<LemmyContext>, context: &Data<LemmyContext>,
language_tags: Vec<String>, language_tags: Vec<String>,
local_user_form: &LocalUserInsertForm, mut local_user_form: LocalUserInsertForm,
local_site_id: SiteId, local_site: &LocalSite,
) -> Result<LocalUser, LemmyError> { ) -> Result<LocalUser, LemmyError> {
let all_languages = Language::read_all(&mut context.pool()).await?; let all_languages = Language::read_all(&mut context.pool()).await?;
// use hashset to avoid duplicates // use hashset to avoid duplicates
let mut language_ids = HashSet::new(); let mut language_ids = HashSet::new();
// Enable languages from `Accept-Language` header // Enable languages from `Accept-Language` header
for l in language_tags { for l in &language_tags {
if let Some(found) = all_languages.iter().find(|all| all.code == l) { if let Some(found) = all_languages.iter().find(|all| &all.code == l) {
language_ids.insert(found.id); language_ids.insert(found.id);
} }
} }
// Enable site languages. Ignored if all languages are enabled. // Enable site languages. Ignored if all languages are enabled.
let discussion_languages = SiteLanguage::read(&mut context.pool(), local_site_id).await?; let discussion_languages = SiteLanguage::read(&mut context.pool(), local_site.site_id).await?;
language_ids.extend(discussion_languages); language_ids.extend(discussion_languages);
let language_ids = language_ids.into_iter().collect(); let language_ids = language_ids.into_iter().collect();
local_user_form.default_listing_type = Some(local_site.default_post_listing_type);
local_user_form.post_listing_mode = Some(local_site.default_post_listing_mode);
// If its the initial site setup, they are an admin
local_user_form.admin = Some(!local_site.site_setup);
local_user_form.interface_language = language_tags.first().cloned();
let inserted_local_user = let inserted_local_user =
LocalUser::create(&mut context.pool(), local_user_form, language_ids).await?; LocalUser::create(&mut context.pool(), &local_user_form, language_ids).await?;
Ok(inserted_local_user) Ok(inserted_local_user)
} }

View file

@ -443,6 +443,7 @@ diesel::table! {
post_downvotes -> FederationModeEnum, post_downvotes -> FederationModeEnum,
comment_upvotes -> FederationModeEnum, comment_upvotes -> FederationModeEnum,
comment_downvotes -> FederationModeEnum, comment_downvotes -> FederationModeEnum,
disable_donation_dialog -> Bool,
} }
} }
@ -514,6 +515,7 @@ diesel::table! {
collapse_bot_comments -> Bool, collapse_bot_comments -> Bool,
default_comment_sort_type -> CommentSortTypeEnum, default_comment_sort_type -> CommentSortTypeEnum,
auto_mark_fetched_posts_as_read -> Bool, auto_mark_fetched_posts_as_read -> Bool,
last_donation_notification -> Timestamptz,
hide_media -> Bool, hide_media -> Bool,
} }
} }

View file

@ -83,6 +83,9 @@ pub struct LocalSite {
pub comment_upvotes: FederationMode, pub comment_upvotes: FederationMode,
/// What kind of comment downvotes your site allows. /// What kind of comment downvotes your site allows.
pub comment_downvotes: FederationMode, pub comment_downvotes: FederationMode,
/// If this is true, users will never see the dialog asking to support Lemmy development with
/// donations.
pub disable_donation_dialog: bool,
} }
#[derive(Clone, derive_new::new)] #[derive(Clone, derive_new::new)]
@ -142,6 +145,8 @@ pub struct LocalSiteInsertForm {
pub comment_upvotes: Option<FederationMode>, pub comment_upvotes: Option<FederationMode>,
#[new(default)] #[new(default)]
pub comment_downvotes: Option<FederationMode>, pub comment_downvotes: Option<FederationMode>,
#[new(default)]
pub disable_donation_dialog: Option<bool>,
} }
#[derive(Clone, Default)] #[derive(Clone, Default)]
@ -175,4 +180,5 @@ pub struct LocalSiteUpdateForm {
pub post_downvotes: Option<FederationMode>, pub post_downvotes: Option<FederationMode>,
pub comment_upvotes: Option<FederationMode>, pub comment_upvotes: Option<FederationMode>,
pub comment_downvotes: Option<FederationMode>, pub comment_downvotes: Option<FederationMode>,
pub disable_donation_dialog: Option<bool>,
} }

View file

@ -8,6 +8,7 @@ use crate::{
PostListingMode, PostListingMode,
PostSortType, PostSortType,
}; };
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
#[cfg(feature = "full")] #[cfg(feature = "full")]
@ -70,6 +71,9 @@ pub struct LocalUser {
pub default_comment_sort_type: CommentSortType, pub default_comment_sort_type: CommentSortType,
/// Whether to automatically mark fetched posts as read. /// Whether to automatically mark fetched posts as read.
pub auto_mark_fetched_posts_as_read: bool, pub auto_mark_fetched_posts_as_read: bool,
/// The last time a donation request was shown to this user. If this is more than a year ago,
/// a new notification request should be shown.
pub last_donation_notification: DateTime<Utc>,
/// Whether to hide posts containing images/videos /// Whether to hide posts containing images/videos
pub hide_media: bool, pub hide_media: bool,
} }
@ -131,6 +135,8 @@ pub struct LocalUserInsertForm {
#[new(default)] #[new(default)]
pub auto_mark_fetched_posts_as_read: Option<bool>, pub auto_mark_fetched_posts_as_read: Option<bool>,
#[new(default)] #[new(default)]
pub last_donation_notification: Option<DateTime<Utc>>,
#[new(default)]
pub hide_media: Option<bool>, pub hide_media: Option<bool>,
} }
@ -164,5 +170,6 @@ pub struct LocalUserUpdateForm {
pub collapse_bot_comments: Option<bool>, pub collapse_bot_comments: Option<bool>,
pub default_comment_sort_type: Option<CommentSortType>, pub default_comment_sort_type: Option<CommentSortType>,
pub auto_mark_fetched_posts_as_read: Option<bool>, pub auto_mark_fetched_posts_as_read: Option<bool>,
pub last_donation_notification: Option<DateTime<Utc>>,
pub hide_media: Option<bool>, pub hide_media: Option<bool>,
} }

View file

@ -235,15 +235,14 @@ mod tests {
password_encrypted: inserted_sara_local_user.password_encrypted, password_encrypted: inserted_sara_local_user.password_encrypted,
open_links_in_new_tab: inserted_sara_local_user.open_links_in_new_tab, open_links_in_new_tab: inserted_sara_local_user.open_links_in_new_tab,
infinite_scroll_enabled: inserted_sara_local_user.infinite_scroll_enabled, infinite_scroll_enabled: inserted_sara_local_user.infinite_scroll_enabled,
admin: false,
post_listing_mode: inserted_sara_local_user.post_listing_mode, post_listing_mode: inserted_sara_local_user.post_listing_mode,
totp_2fa_enabled: inserted_sara_local_user.totp_2fa_enabled, totp_2fa_enabled: inserted_sara_local_user.totp_2fa_enabled,
enable_keyboard_navigation: inserted_sara_local_user.enable_keyboard_navigation, enable_keyboard_navigation: inserted_sara_local_user.enable_keyboard_navigation,
enable_animated_images: inserted_sara_local_user.enable_animated_images, enable_animated_images: inserted_sara_local_user.enable_animated_images,
enable_private_messages: inserted_sara_local_user.enable_private_messages, enable_private_messages: inserted_sara_local_user.enable_private_messages,
collapse_bot_comments: inserted_sara_local_user.collapse_bot_comments, collapse_bot_comments: inserted_sara_local_user.collapse_bot_comments,
auto_mark_fetched_posts_as_read: false, last_donation_notification: inserted_sara_local_user.last_donation_notification,
hide_media: false, ..Default::default()
}, },
creator: Person { creator: Person {
id: inserted_sara_person.id, id: inserted_sara_person.id,

View file

@ -12,17 +12,14 @@ use url::Url;
#[serde(default)] #[serde(default)]
pub struct Settings { pub struct Settings {
/// settings related to the postgresql database /// settings related to the postgresql database
#[default(Default::default())]
pub database: DatabaseConfig, pub database: DatabaseConfig,
/// Pictrs image server configuration. /// Pictrs image server configuration.
#[default(Some(Default::default()))] #[default(Some(Default::default()))]
pub(crate) pictrs: Option<PictrsConfig>, pub(crate) pictrs: Option<PictrsConfig>,
/// Email sending configuration. All options except login/password are mandatory /// Email sending configuration. All options except login/password are mandatory
#[default(None)]
#[doku(example = "Some(Default::default())")] #[doku(example = "Some(Default::default())")]
pub email: Option<EmailConfig>, pub email: Option<EmailConfig>,
/// Parameters for automatic configuration of new instance (only used at first start) /// Parameters for automatic configuration of new instance (only used at first start)
#[default(None)]
#[doku(example = "Some(Default::default())")] #[doku(example = "Some(Default::default())")]
pub setup: Option<SetupConfig>, pub setup: Option<SetupConfig>,
/// the domain name of your instance (mandatory) /// the domain name of your instance (mandatory)
@ -41,18 +38,14 @@ pub struct Settings {
pub tls_enabled: bool, pub tls_enabled: bool,
/// Set the URL for opentelemetry exports. If you do not have an opentelemetry collector, do not /// Set the URL for opentelemetry exports. If you do not have an opentelemetry collector, do not
/// set this option /// set this option
#[default(None)]
#[doku(skip)] #[doku(skip)]
pub opentelemetry_url: Option<Url>, pub opentelemetry_url: Option<Url>,
#[default(Default::default())]
pub federation: FederationWorkerConfig, pub federation: FederationWorkerConfig,
// Prometheus configuration. // Prometheus configuration.
#[default(None)]
#[doku(example = "Some(Default::default())")] #[doku(example = "Some(Default::default())")]
pub prometheus: Option<PrometheusConfig>, pub prometheus: Option<PrometheusConfig>,
/// Sets a response Access-Control-Allow-Origin CORS header /// Sets a response Access-Control-Allow-Origin CORS header
/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin /// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
#[default(None)]
#[doku(example = "lemmy.tld")] #[doku(example = "lemmy.tld")]
cors_origin: Option<String>, cors_origin: Option<String>,
} }
@ -74,7 +67,6 @@ pub struct PictrsConfig {
pub url: Url, pub url: Url,
/// Set a custom pictrs API key. ( Required for deleting images ) /// Set a custom pictrs API key. ( Required for deleting images )
#[default(None)]
pub api_key: Option<String>, pub api_key: Option<String>,
/// Specifies how to handle remote images, so that users don't have to connect directly to remote /// Specifies how to handle remote images, so that users don't have to connect directly to remote
@ -114,7 +106,7 @@ pub struct PictrsConfig {
pub image_upload_disabled: bool, pub image_upload_disabled: bool,
} }
#[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document, PartialEq)] #[derive(Debug, Deserialize, Serialize, Clone, Default, Document, PartialEq)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub enum PictrsImageMode { pub enum PictrsImageMode {
/// Leave images unchanged, don't generate any local thumbnails for post urls. Instead the /// Leave images unchanged, don't generate any local thumbnails for post urls. Instead the
@ -185,7 +177,7 @@ impl EmailConfig {
} }
} }
#[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document)] #[derive(Debug, Deserialize, Serialize, Clone, Default, Document)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct SetupConfig { pub struct SetupConfig {
/// Username for the admin user /// Username for the admin user
@ -199,7 +191,6 @@ pub struct SetupConfig {
pub site_name: String, pub site_name: String,
/// Email for the admin user (optional, can be omitted and set later through the website) /// Email for the admin user (optional, can be omitted and set later through the website)
#[doku(example = "user@example.com")] #[doku(example = "user@example.com")]
#[default(None)]
pub admin_email: Option<String>, pub admin_email: Option<String>,
} }

View file

@ -0,0 +1,6 @@
ALTER TABLE local_user
DROP COLUMN last_donation_notification;
ALTER TABLE local_site
DROP COLUMN disable_donation_dialog;

View file

@ -0,0 +1,8 @@
-- Generate new column last_donation_notification with default value at random time in the
-- past year (so that users dont see it all at the same time after instance upgrade).
ALTER TABLE local_user
ADD COLUMN last_donation_notification timestamptz NOT NULL DEFAULT (now() - (random() * (interval '12 months')));
ALTER TABLE local_site
ADD COLUMN disable_donation_dialog boolean NOT NULL DEFAULT FALSE;

View file

@ -26,6 +26,7 @@ use lemmy_api::{
block::user_block_person, block::user_block_person,
change_password::change_password, change_password::change_password,
change_password_after_reset::change_password_after_reset, change_password_after_reset::change_password_after_reset,
donation_dialog_shown::donation_dialog_shown,
generate_totp_secret::generate_totp_secret, generate_totp_secret::generate_totp_secret,
get_captcha::get_captcha, get_captcha::get_captcha,
list_banned::list_banned_users, list_banned::list_banned_users,
@ -331,6 +332,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) {
.route("/unread_count", get().to(unread_count)) .route("/unread_count", get().to(unread_count))
.route("/list_logins", get().to(list_logins)) .route("/list_logins", get().to(list_logins))
.route("/validate_auth", get().to(validate_auth)) .route("/validate_auth", get().to(validate_auth))
.route("/donation_dialog_shown", post().to(donation_dialog_shown))
.route("/avatar", post().to(upload_user_avatar)) .route("/avatar", post().to(upload_user_avatar))
.route("/avatar", delete().to(delete_user_avatar)) .route("/avatar", delete().to(delete_user_avatar))
.route("/banner", post().to(upload_user_banner)) .route("/banner", post().to(upload_user_banner))