add: last_online and online indicators
This commit is contained in:
parent
d3d0c41334
commit
e0a6072cc4
12 changed files with 177 additions and 9 deletions
|
@ -24,6 +24,7 @@
|
||||||
--color-shadow: rgba(0, 0, 0, 0.08);
|
--color-shadow: rgba(0, 0, 0, 0.08);
|
||||||
--color-red: hsl(0, 84%, 40%);
|
--color-red: hsl(0, 84%, 40%);
|
||||||
--color-green: hsl(100, 84%, 20%);
|
--color-green: hsl(100, 84%, 20%);
|
||||||
|
--color-yellow: hsl(41, 63%, 75%);
|
||||||
--radius: 6px;
|
--radius: 6px;
|
||||||
--circle: 360px;
|
--circle: 360px;
|
||||||
--shadow-x-offset: 0;
|
--shadow-x-offset: 0;
|
||||||
|
@ -54,6 +55,7 @@
|
||||||
--color-link: #93c5fd;
|
--color-link: #93c5fd;
|
||||||
--color-red: hsl(0, 94%, 82%);
|
--color-red: hsl(0, 94%, 82%);
|
||||||
--color-green: hsl(100, 94%, 82%);
|
--color-green: hsl(100, 94%, 82%);
|
||||||
|
--color-yellow: hsl(41, 63%, 65%);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
|
@ -111,10 +111,14 @@ show_community=true) -%} {% if community and show_community %}
|
||||||
|
|
||||||
<div class="flex flex-col w-full gap-1">
|
<div class="flex flex-col w-full gap-1">
|
||||||
<div class="flex flex-wrap gap-2 items-center">
|
<div class="flex flex-wrap gap-2 items-center">
|
||||||
|
<div class="flex">
|
||||||
<a href="/@{{ owner.username }}"
|
<a href="/@{{ owner.username }}"
|
||||||
>{{ components::username(user=owner) }}</a
|
>{{ components::username(user=owner) }}</a
|
||||||
>
|
>
|
||||||
|
|
||||||
|
{{ components::online_indicator(user=owner) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<span class="fade date">{{ post.created }}</span>
|
<span class="fade date">{{ post.created }}</span>
|
||||||
|
|
||||||
{% if show_community %}
|
{% if show_community %}
|
||||||
|
@ -274,4 +278,45 @@ show_community=true) -%} {% if community and show_community %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{%- endmacro %}
|
{%- endmacro %} {% macro online_indicator(user) -%} {% if not
|
||||||
|
user.settings.private_last_online or is_helper %}
|
||||||
|
<div
|
||||||
|
class="online_indicator"
|
||||||
|
style="display: contents"
|
||||||
|
hook="online_indicator"
|
||||||
|
hook-arg:last_seen="{{ user.last_seen }}"
|
||||||
|
>
|
||||||
|
<div style="display: none" hook_ui_ident="online" title="Online">
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
style="fill: var(--color-green)"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="6"></circle>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: none" hook_ui_ident="idle" title="Idle">
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
style="fill: var(--color-yellow)"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="6"></circle>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: none" hook_ui_ident="offline" title="Offline">
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
style="fill: hsl(0, 0%, 50%)"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="6"></circle>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %} {%- endmacro %}
|
||||||
|
|
|
@ -72,6 +72,14 @@
|
||||||
<span class="notification chip">Joined</span>
|
<span class="notification chip">Joined</span>
|
||||||
<span class="date">{{ profile.created }}</span>
|
<span class="date">{{ profile.created }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if not profile.settings.private_last_seen or is_self
|
||||||
|
or is_helper %}
|
||||||
|
<div class="w-full flex justify-between items-center">
|
||||||
|
<span class="notification chip">Last seen</span>
|
||||||
|
<span class="date">{{ profile.last_seen }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -493,6 +493,11 @@
|
||||||
"{{ profile.settings.private_communities }}",
|
"{{ profile.settings.private_communities }}",
|
||||||
"checkbox",
|
"checkbox",
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
["private_last_seen", "Keep my last seen time private"],
|
||||||
|
"{{ profile.settings.private_last_seen }}",
|
||||||
|
"checkbox",
|
||||||
|
],
|
||||||
],
|
],
|
||||||
settings,
|
settings,
|
||||||
);
|
);
|
||||||
|
|
|
@ -103,6 +103,7 @@
|
||||||
atto["hooks::character_counter.init"]();
|
atto["hooks::character_counter.init"]();
|
||||||
atto["hooks::long_text.init"]();
|
atto["hooks::long_text.init"]();
|
||||||
atto["hooks::alt"]();
|
atto["hooks::alt"]();
|
||||||
|
atto["hooks::online_indicator"]();
|
||||||
// atto["hooks::ips"]();
|
// atto["hooks::ips"]();
|
||||||
atto["hooks::check_reactions"]();
|
atto["hooks::check_reactions"]();
|
||||||
atto["hooks::tabs"]();
|
atto["hooks::tabs"]();
|
||||||
|
@ -110,6 +111,14 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{% if user %}
|
||||||
|
<script data-turbo-permanent="true" id="update-seen-script">
|
||||||
|
document.documentElement.addEventListener("turbo:load", () => {
|
||||||
|
trigger("me::seen");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- dialogs -->
|
<!-- dialogs -->
|
||||||
<dialog id="link_filter">
|
<dialog id="link_filter">
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
|
|
|
@ -371,6 +371,43 @@ media_theme_pref();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.define("last_seen_just_now", (_, last_seen) => {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const maximum_time_to_be_considered_online = 60000 * 2; // 2 minutes
|
||||||
|
return now - last_seen <= maximum_time_to_be_considered_online;
|
||||||
|
});
|
||||||
|
|
||||||
|
self.define("last_seen_recently", (_, last_seen) => {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const maximum_time_to_be_considered_idle = 60000 * 5; // 5 minutes
|
||||||
|
return now - last_seen <= maximum_time_to_be_considered_idle;
|
||||||
|
});
|
||||||
|
|
||||||
|
self.define("hooks::online_indicator", ({ $ }) => {
|
||||||
|
for (const element of Array.from(
|
||||||
|
document.querySelectorAll("[hook=online_indicator]") || [],
|
||||||
|
)) {
|
||||||
|
const last_seen = Number.parseInt(
|
||||||
|
element.getAttribute("hook-arg:last_seen"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const is_online = $.last_seen_just_now(last_seen);
|
||||||
|
const is_idle = $.last_seen_recently(last_seen);
|
||||||
|
|
||||||
|
const offline = element.querySelector("[hook_ui_ident=offline]");
|
||||||
|
const online = element.querySelector("[hook_ui_ident=online]");
|
||||||
|
const idle = element.querySelector("[hook_ui_ident=idle]");
|
||||||
|
|
||||||
|
if (is_online) {
|
||||||
|
online.style.display = "contents";
|
||||||
|
} else if (is_idle) {
|
||||||
|
idle.style.display = "contents";
|
||||||
|
} else {
|
||||||
|
offline.style.display = "contents";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
self.define(
|
self.define(
|
||||||
"hooks::attach_to_partial",
|
"hooks::attach_to_partial",
|
||||||
({ $ }, partial, full, attach, wrapper, page, run_on_load) => {
|
({ $ }, partial, full, attach, wrapper, page, run_on_load) => {
|
||||||
|
|
|
@ -150,4 +150,16 @@
|
||||||
`/mod_panel/file_report?asset=${asset}&asset_type=${asset_type}`,
|
`/mod_panel/file_report?asset=${asset}&asset_type=${asset_type}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.define("seen", () => {
|
||||||
|
fetch("/api/v1/auth/profile/me/seen", {
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
trigger("atto::toast", ["error", res.message]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -196,6 +196,24 @@ pub async fn update_user_role_request(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the current user's last seen value.
|
||||||
|
pub async fn seen_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data) {
|
||||||
|
Some(ua) => ua,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match data.seen_user(&user).await {
|
||||||
|
Ok(_) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "User updated".to_string(),
|
||||||
|
payload: (),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Delete the given user.
|
/// Delete the given user.
|
||||||
pub async fn delete_user_request(
|
pub async fn delete_user_request(
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
|
|
|
@ -147,6 +147,7 @@ pub fn routes() -> Router {
|
||||||
"/auth/profile/{id}/verified",
|
"/auth/profile/{id}/verified",
|
||||||
post(auth::profile::update_user_is_verified_request),
|
post(auth::profile::update_user_is_verified_request),
|
||||||
)
|
)
|
||||||
|
.route("/auth/profile/me/seen", post(auth::profile::seen_request))
|
||||||
.route(
|
.route(
|
||||||
"/auth/profile/find/{id}",
|
"/auth/profile/find/{id}",
|
||||||
get(auth::profile::redirect_from_id),
|
get(auth::profile::redirect_from_id),
|
||||||
|
|
|
@ -10,6 +10,7 @@ use crate::{auto_method, execute, get, query_row};
|
||||||
use pathbufd::PathBufD;
|
use pathbufd::PathBufD;
|
||||||
use std::fs::{exists, remove_file};
|
use std::fs::{exists, remove_file};
|
||||||
use tetratto_shared::hash::{hash_salted, salt};
|
use tetratto_shared::hash::{hash_salted, salt};
|
||||||
|
use tetratto_shared::unix_epoch_timestamp;
|
||||||
|
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
use rusqlite::Row;
|
use rusqlite::Row;
|
||||||
|
@ -33,10 +34,10 @@ impl DataManager {
|
||||||
tokens: serde_json::from_str(&get!(x->6(String)).to_string()).unwrap(),
|
tokens: serde_json::from_str(&get!(x->6(String)).to_string()).unwrap(),
|
||||||
permissions: FinePermission::from_bits(get!(x->7(u32))).unwrap(),
|
permissions: FinePermission::from_bits(get!(x->7(u32))).unwrap(),
|
||||||
is_verified: if get!(x->8(i8)) == 1 { true } else { false },
|
is_verified: if get!(x->8(i8)) == 1 { true } else { false },
|
||||||
// counts
|
|
||||||
notification_count: get!(x->9(isize)) as usize,
|
notification_count: get!(x->9(isize)) as usize,
|
||||||
follower_count: get!(x->10(isize)) as usize,
|
follower_count: get!(x->10(isize)) as usize,
|
||||||
following_count: get!(x->11(isize)) as usize,
|
following_count: get!(x->11(isize)) as usize,
|
||||||
|
last_seen: get!(x->12(isize)) as usize,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +105,7 @@ impl DataManager {
|
||||||
|
|
||||||
let res = execute!(
|
let res = execute!(
|
||||||
&conn,
|
&conn,
|
||||||
"INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
|
"INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)",
|
||||||
&[
|
&[
|
||||||
&data.id.to_string().as_str(),
|
&data.id.to_string().as_str(),
|
||||||
&data.created.to_string().as_str(),
|
&data.created.to_string().as_str(),
|
||||||
|
@ -117,7 +118,8 @@ impl DataManager {
|
||||||
&(if data.is_verified { 1 } else { 0 }).to_string().as_str(),
|
&(if data.is_verified { 1 } else { 0 }).to_string().as_str(),
|
||||||
&0.to_string().as_str(),
|
&0.to_string().as_str(),
|
||||||
&0.to_string().as_str(),
|
&0.to_string().as_str(),
|
||||||
&0.to_string().as_str()
|
&0.to_string().as_str(),
|
||||||
|
&data.last_seen.to_string().as_str(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -399,6 +401,30 @@ impl DataManager {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn seen_user(&self, user: &User) -> Result<()> {
|
||||||
|
let conn = match self.connect().await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = execute!(
|
||||||
|
&conn,
|
||||||
|
"UPDATE users SET last_seen = $1 WHERE id = $2",
|
||||||
|
&[
|
||||||
|
&unix_epoch_timestamp().to_string().as_str(),
|
||||||
|
&user.id.to_string().as_str()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cache_clear_user(&user).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn cache_clear_user(&self, user: &User) {
|
pub async fn cache_clear_user(&self, user: &User) {
|
||||||
self.2.remove(format!("atto.user:{}", user.id)).await;
|
self.2.remove(format!("atto.user:{}", user.id)).await;
|
||||||
self.2.remove(format!("atto.user:{}", user.username)).await;
|
self.2.remove(format!("atto.user:{}", user.username)).await;
|
||||||
|
|
|
@ -8,8 +8,8 @@ CREATE TABLE IF NOT EXISTS users (
|
||||||
tokens TEXT NOT NULL,
|
tokens TEXT NOT NULL,
|
||||||
permissions INTEGER NOT NULL,
|
permissions INTEGER NOT NULL,
|
||||||
verified INTEGER NOT NULL,
|
verified INTEGER NOT NULL,
|
||||||
-- counts
|
|
||||||
notification_count INTEGER NOT NULL,
|
notification_count INTEGER NOT NULL,
|
||||||
follower_count INTEGER NOT NULL,
|
follower_count INTEGER NOT NULL,
|
||||||
following_count INTEGER NOT NULL
|
following_count INTEGER NOT NULL,
|
||||||
|
last_seen INTEGER NOT NULL
|
||||||
)
|
)
|
||||||
|
|
|
@ -23,6 +23,7 @@ pub struct User {
|
||||||
pub notification_count: usize,
|
pub notification_count: usize,
|
||||||
pub follower_count: usize,
|
pub follower_count: usize,
|
||||||
pub following_count: usize,
|
pub following_count: usize,
|
||||||
|
pub last_seen: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
@ -50,6 +51,8 @@ pub struct UserSettings {
|
||||||
pub private_communities: bool,
|
pub private_communities: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub theme_preference: ThemePreference,
|
pub theme_preference: ThemePreference,
|
||||||
|
#[serde(default)]
|
||||||
|
pub private_last_seen: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UserSettings {
|
impl Default for UserSettings {
|
||||||
|
@ -60,6 +63,7 @@ impl Default for UserSettings {
|
||||||
private_profile: false,
|
private_profile: false,
|
||||||
private_communities: false,
|
private_communities: false,
|
||||||
theme_preference: ThemePreference::default(),
|
theme_preference: ThemePreference::default(),
|
||||||
|
private_last_seen: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,6 +96,7 @@ impl User {
|
||||||
notification_count: 0,
|
notification_count: 0,
|
||||||
follower_count: 0,
|
follower_count: 0,
|
||||||
following_count: 0,
|
following_count: 0,
|
||||||
|
last_seen: unix_epoch_timestamp() as usize,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue