diff --git a/crates/app/src/public/html/chats/channels.lisp b/crates/app/src/public/html/chats/channels.lisp
index fb61be0..ced2f93 100644
--- a/crates/app/src/public/html/chats/channels.lisp
+++ b/crates/app/src/public/html/chats/channels.lisp
@@ -24,32 +24,18 @@
(text "{{ icon \"ellipsis\" }}"))
(div
("class" "inner")
- (text "{% if user.id == channel.owner -%} {% if selected_community == 0 %}")
(button
- ("class" "lowered small")
+ ("onclick" "trigger('atto::copy_text', ['{{ channel.id }}'])")
+ (icon (text "copy"))
+ (str (text "general:action.copy_id")))
+ (text "{% if user.id == channel.owner or can_manage_channels -%}")
+ ; owner/manager controls
+ (button
("onclick" "add_member('{{ channel.id }}')")
(text "{{ icon \"user-plus\" }}")
(span
(text "{{ text \"chats:action.add_someone\" }}")))
- ; mute/unmute
(button
- ("class" "lowered small {% if channel.id in user.channel_mutes -%} hidden {%- endif %}")
- ("ui_ident" "channel.mute:{{ channel.id }}")
- ("onclick" "mute_channel('{{ channel.id }}')")
- (icon (text "bell-off"))
- (span
- (str (text "chats:action.mute"))))
- (button
- ("class" "lowered small {% if not channel.id in user.channel_mutes -%} hidden {%- endif %}")
- ("ui_ident" "channel.unmute:{{ channel.id }}")
- ("onclick" "mute_channel('{{ channel.id }}', false)")
- (icon (text "bell-ring"))
- (span
- (str (text "chats:action.unmute"))))
- ; ...
- (text "{%- endif %}")
- (button
- ("class" "lowered small")
("onclick" "update_channel_title('{{ channel.id }}')")
(text "{{ icon \"pencil\" }}")
(span
@@ -60,14 +46,32 @@
(text "{{ icon \"trash\" }}")
(span
(text "{{ text \"general:action.delete\" }}")))
- (text "{% elif selected_community == 0 %}")
+ (text "{%- endif %} {% if selected_community == 0 %}")
+ ; mute/unmute
+ (button
+ ("class" "{% if channel.id in user.channel_mutes -%} hidden {%- endif %}")
+ ("ui_ident" "channel.mute:{{ channel.id }}")
+ ("onclick" "mute_channel('{{ channel.id }}')")
+ (icon (text "bell-off"))
+ (span
+ (str (text "chats:action.mute"))))
+ (button
+ ("class" "{% if not channel.id in user.channel_mutes -%} hidden {%- endif %}")
+ ("ui_ident" "channel.unmute:{{ channel.id }}")
+ ("onclick" "mute_channel('{{ channel.id }}', false)")
+ (icon (text "bell-ring"))
+ (span
+ (str (text "chats:action.unmute"))))
+ ; ...
+ (text "{% if user.id != channel.owner -%}")
+ ; group chat member controls
(button
("onclick" "kick_member('{{ channel.id }}', '{{ user.id }}')")
("class" "red")
(text "{{ icon \"door-open\" }}")
(span
(text "{{ text \"chats:action.leave\" }}")))
- (text "{%- endif %}"))))
+ (text "{%- endif %} {%- endif %}"))))
(text "{% endfor %}"))
(text "{% if selected_community == 0 and selected_channel -%}")
(div
diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp
index c797ea8..6b2832a 100644
--- a/crates/app/src/public/html/components.lisp
+++ b/crates/app/src/public/html/components.lisp
@@ -1210,6 +1210,8 @@
(text "{%- endif %}"))
(div
("class" "flex gap_2 hidden")
+ ("onclick" "window.EMOJI_PICKER_REACTION_MESSAGE_ID = '{{ message.id }}'")
+ (text "{{ self::emoji_picker(element_id=\"react_emoji_picker_field\", render_dialog=false, render_button=true, small=true) }}")
(text "{{ self::message_actions(owner=user, message=message, can_manage_message=can_manage_message) }}")))
(text "{%- endif %}")
(div
@@ -1232,14 +1234,12 @@
("ui_ident" "emoji_{{ emoji }}")
("onclick" "trigger('me::message_react', [event.target.parentElement.parentElement, '{{ message.id }}', '{{ emoji }}'])")
(span (text "{{ emoji|emojis|safe }} {{ num }}")))
- (text "{%- endfor %}")
-
- (div
- ("class" "hidden")
- (text "{{ self::emoji_picker(element_id=\"react_emoji_picker_field\", render_dialog=false, render_button=true, small=true) }}"))))
+ (text "{%- endfor %}")))
(text "{% if grouped -%}")
(div
- ("class" "hidden")
+ ("class" "hidden flex gap_2 items_center")
+ ("onclick" "window.EMOJI_PICKER_REACTION_MESSAGE_ID = '{{ message.id }}'")
+ (text "{{ self::emoji_picker(element_id=\"react_emoji_picker_field\", render_dialog=false, render_button=true, small=true) }}")
(text "{{ self::message_actions(owner=user, message=message, can_manage_message=can_manage_message) }}"))
(text "{%- endif %}"))))
@@ -2740,7 +2740,7 @@
(text "{%- endmacro %}")
(text "{% macro advertisement(size=\"Leaderboard\") -%}")
-(text "{% if not is_supporter and config.enable_user_ads -%}")
+(text "{% if user and not is_supporter and config.enable_user_ads -%}")
(object ("class" "tetratto_ad") ("data-ad-size" "{{ size }}"))
(text "{%- endif %}")
(text "{%- endmacro %}")
diff --git a/crates/app/src/routes/api/v1/ads.rs b/crates/app/src/routes/api/v1/ads.rs
index 15c80bd..505b49e 100644
--- a/crates/app/src/routes/api/v1/ads.rs
+++ b/crates/app/src/routes/api/v1/ads.rs
@@ -6,8 +6,9 @@ use crate::{
};
use axum::{
extract::Path,
- response::{IntoResponse, Redirect},
+ response::{Html, IntoResponse},
Extension, Json,
+ http::StatusCode,
};
use tetratto_core::model::{
economy::UserAd,
@@ -139,8 +140,56 @@ pub async fn click_request(
let data = &(data.read().await).0;
let user = get_user_from_token!(jar, data);
+ let bad_return = (
+ StatusCode::FOUND,
+ [
+ (
+ "Set-Cookie".to_string(),
+ format!(
+ "Atto-Clicked=true; Path=/api/v1/ads/host/{host}/{id}/click; Max-Age=86400"
+ ),
+ ),
+ ("Location".to_string(), data.0.0.host.clone()),
+ ],
+ Html(""),
+ );
+
+ if jar.get("Atto-Clicked").is_some() {
+ // we've already clicked this ad, don't charge
+ match data.get_ad_by_id(id).await {
+ Ok(x) => {
+ return (
+ StatusCode::FOUND,
+ [
+ (
+ "Set-Cookie".to_string(),
+ format!(
+ "Atto-Clicked=true; Path=/api/v1/ads/host/{host}/{id}/click; Max-Age=86400"
+ ),
+ ),
+ ("Location".to_string(), x.target),
+ ],
+ Html(""),
+ );
+ }
+ Err(_) => return bad_return,
+ }
+ }
+
match data.ad_click(host, id, user).await {
- Ok(t) => Redirect::to(&t),
- Err(_) => Redirect::to(&data.0.0.host),
+ Ok(t) => (
+ StatusCode::FOUND,
+ [
+ (
+ "Set-Cookie".to_string(),
+ format!(
+ "Atto-Clicked=true; Path=/api/v1/ads/host/{host}/{id}/click; Max-Age=86400"
+ ),
+ ),
+ ("Location".to_string(), t),
+ ],
+ Html(""),
+ ),
+ Err(_) => bad_return,
}
}
diff --git a/crates/app/src/routes/pages/chats.rs b/crates/app/src/routes/pages/chats.rs
index 65ff437..4134888 100644
--- a/crates/app/src/routes/pages/chats.rs
+++ b/crates/app/src/routes/pages/chats.rs
@@ -348,12 +348,25 @@ pub async fn channels_request(
None
};
+ let membership = match data
+ .0
+ .get_membership_by_owner_community(user.id, community)
+ .await
+ {
+ Ok(m) => m,
+ Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
+ };
+
+ let can_manage_channels = membership.role.check(CommunityPermission::MANAGE_CHANNELS)
+ | user.permissions.check(FinePermission::MANAGE_CHANNELS);
+
let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
context.insert("channels", &channels);
context.insert("page", &props.page);
+ context.insert("can_manage_channels", &can_manage_channels);
context.insert("members", &members);
context.insert("channel", &channel);
diff --git a/crates/app/src/routes/pages/economy.rs b/crates/app/src/routes/pages/economy.rs
index 97fc264..6f0c419 100644
--- a/crates/app/src/routes/pages/economy.rs
+++ b/crates/app/src/routes/pages/economy.rs
@@ -5,7 +5,7 @@ use axum::{
};
use crate::cookie::CookieJar;
use tetratto_core::model::{
- economy::{CoinTransferMethod, UserAd, UserAdSize},
+ economy::{CoinTransferMethod, UserAdSize},
Error,
};
use crate::{assets::initial_context, get_lang, get_user_from_token, State};
@@ -254,18 +254,17 @@ pub async fn random_ad_request(
Query(props): Query,
) -> impl IntoResponse {
let data = data.read().await;
+ let headers = [(
+ "content-security-policy",
+ "default-src 'self' *.spotify.com musicbrainz.org; img-src * data:; media-src *; font-src *; style-src 'unsafe-inline' 'self' *; script-src 'self' 'unsafe-inline' *; worker-src * blob:; object-src 'self' *; upgrade-insecure-requests; connect-src * localhost; frame-src 'self' blob: *; frame-ancestors *",
+ )];
+
let ad = match data.0.random_ad_charged(props.size.clone()).await {
Ok(x) => x,
- Err(_) => UserAd {
- id: 0,
- created: 0,
- upload_id: 0,
- owner: data.0.0.0.system_user,
- target: data.0.0.0.host.clone(),
- last_charge_time: 0,
- is_running: true,
- size: props.size,
- },
+ Err(_) => {
+ // no ad found, show nothing
+ return (headers, Html(String::new()));
+ }
};
let mut context = tera::Context::new();
@@ -276,10 +275,7 @@ pub async fn random_ad_request(
// return
(
- [(
- "content-security-policy",
- "default-src 'self' *.spotify.com musicbrainz.org; img-src * data:; media-src *; font-src *; style-src 'unsafe-inline' 'self' *; script-src 'self' 'unsafe-inline' *; worker-src * blob:; object-src 'self' *; upgrade-insecure-requests; connect-src * localhost; frame-src 'self' blob: *; frame-ancestors *",
- )],
+ headers,
Html(data.1.render("economy/ad.html", &context).unwrap()),
)
}
@@ -291,19 +287,17 @@ pub async fn known_ad_request(
Path(id): Path,
) -> impl IntoResponse {
let data = data.read().await;
+ let headers = [
+ (
+ "content-security-policy",
+ "default-src 'self' *.spotify.com musicbrainz.org; img-src * data:; media-src *; font-src *; style-src 'unsafe-inline' 'self' *; script-src 'self' 'unsafe-inline' *; worker-src * blob:; object-src 'self' *; upgrade-insecure-requests; connect-src * localhost; frame-src 'self' blob: *; frame-ancestors *",
+ ),
+ ("Cache-Control", "no-cache"),
+ ];
+
let ad = match data.0.get_ad_by_id(id).await {
Ok(x) => x,
- Err(_) => UserAd {
- // polyfill ad
- id: 0,
- created: 0,
- upload_id: 0,
- owner: data.0.0.0.system_user,
- target: data.0.0.0.host.clone(),
- last_charge_time: 0,
- is_running: true,
- size: props.size,
- },
+ Err(_) => return (headers, Html(String::new())),
};
let mut context = tera::Context::new();
@@ -314,13 +308,7 @@ pub async fn known_ad_request(
// return
(
- [
- (
- "content-security-policy",
- "default-src 'self' *.spotify.com musicbrainz.org; img-src * data:; media-src *; font-src *; style-src 'unsafe-inline' 'self' *; script-src 'self' 'unsafe-inline' *; worker-src * blob:; object-src 'self' *; upgrade-insecure-requests; connect-src * localhost; frame-src 'self' blob: *; frame-ancestors *",
- ),
- ("Cache-Control", "no-cache"),
- ],
+ headers,
Html(data.1.render("economy/ad.html", &context).unwrap()),
)
}