federation refinement
This commit is contained in:
@@ -5,7 +5,7 @@ use application::ports::{
|
||||
NewReviewPageData, ProfilePageData, RegisterPageData, UsersPageData,
|
||||
};
|
||||
use domain::models::{
|
||||
DiaryEntry, FeedEntry, MonthActivity, MonthlyRating, ReviewSource, UserStats, UserSummary,
|
||||
DiaryEntry, FeedEntry, MonthActivity, MonthlyRating, ReviewSource, UserStats,
|
||||
UserTrends, collections::Paginated,
|
||||
};
|
||||
|
||||
@@ -81,10 +81,18 @@ struct ActivityFeedTemplate<'a> {
|
||||
page_items: Vec<PageItem>,
|
||||
}
|
||||
|
||||
struct UserSummaryView {
|
||||
user_id: uuid::Uuid,
|
||||
display_name: String,
|
||||
initial: char,
|
||||
avg_rating_display: String,
|
||||
total_movies: i64,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "users.html")]
|
||||
struct UsersTemplate<'a> {
|
||||
users: &'a [UserSummary],
|
||||
users: Vec<UserSummaryView>,
|
||||
ctx: &'a HtmlPageContext,
|
||||
}
|
||||
|
||||
@@ -100,6 +108,9 @@ struct ProfileTemplate<'a> {
|
||||
profile_display_name: String,
|
||||
profile_user_id: uuid::Uuid,
|
||||
stats: &'a UserStats,
|
||||
avg_rating_display: String,
|
||||
favorite_director_display: String,
|
||||
most_active_month_display: String,
|
||||
view: &'a str,
|
||||
entries: Option<&'a Paginated<DiaryEntry>>,
|
||||
current_offset: u32,
|
||||
@@ -113,6 +124,7 @@ struct ProfileTemplate<'a> {
|
||||
is_own_profile: bool,
|
||||
error: Option<String>,
|
||||
following_count: usize,
|
||||
pending_followers: Vec<RemoteActorData>,
|
||||
}
|
||||
|
||||
struct RemoteActorData {
|
||||
@@ -255,8 +267,23 @@ impl HtmlRenderer for AskamaHtmlRenderer {
|
||||
}
|
||||
|
||||
fn render_users_page(&self, data: UsersPageData) -> Result<String, String> {
|
||||
let users: Vec<UserSummaryView> = data.users.iter().map(|u| {
|
||||
let email = u.email();
|
||||
let display_name = email.split('@').next().unwrap_or(email).to_string();
|
||||
let initial = display_name.chars().next().unwrap_or('?').to_ascii_uppercase();
|
||||
let avg_rating_display = u.avg_rating
|
||||
.map(|r| format!("{:.1}", r))
|
||||
.unwrap_or_else(|| "—".to_string());
|
||||
UserSummaryView {
|
||||
user_id: u.user_id.value(),
|
||||
display_name,
|
||||
initial,
|
||||
avg_rating_display,
|
||||
total_movies: u.total_movies,
|
||||
}
|
||||
}).collect();
|
||||
UsersTemplate {
|
||||
users: &data.users,
|
||||
users,
|
||||
ctx: &data.ctx,
|
||||
}
|
||||
.render()
|
||||
@@ -279,11 +306,25 @@ impl HtmlRenderer for AskamaHtmlRenderer {
|
||||
.map(|e| if e.limit > 0 { ((e.total_count + e.limit as u64 - 1) / e.limit as u64) as u32 } else { 0 })
|
||||
.unwrap_or(0);
|
||||
let current_page = if data.limit > 0 { data.current_offset / data.limit } else { 0 };
|
||||
let avg_rating_display = data.stats.avg_rating
|
||||
.map(|r| format!("{:.1}", r))
|
||||
.unwrap_or_else(|| "—".to_string());
|
||||
let favorite_director_display = data.stats.favorite_director
|
||||
.as_deref()
|
||||
.unwrap_or("—")
|
||||
.to_string();
|
||||
let most_active_month_display = data.stats.most_active_month
|
||||
.as_deref()
|
||||
.unwrap_or("—")
|
||||
.to_string();
|
||||
ProfileTemplate {
|
||||
ctx: &data.ctx,
|
||||
profile_display_name,
|
||||
profile_user_id: data.profile_user_id,
|
||||
stats: &data.stats,
|
||||
avg_rating_display,
|
||||
favorite_director_display,
|
||||
most_active_month_display,
|
||||
view: &data.view,
|
||||
entries: data.entries.as_ref(),
|
||||
current_offset: data.current_offset,
|
||||
@@ -297,6 +338,11 @@ impl HtmlRenderer for AskamaHtmlRenderer {
|
||||
is_own_profile: data.is_own_profile,
|
||||
error: data.error,
|
||||
following_count: data.following_count,
|
||||
pending_followers: data.pending_followers.into_iter().map(|a| RemoteActorData {
|
||||
handle: a.handle,
|
||||
url: a.url,
|
||||
display_name: a.display_name,
|
||||
}).collect(),
|
||||
}
|
||||
.render()
|
||||
.map_err(|e| e.to_string())
|
||||
|
||||
@@ -25,12 +25,14 @@
|
||||
<div class="comment">{{ comment.value() }}</div>
|
||||
{% endif %}
|
||||
<div class="feed-meta">
|
||||
<a href="/users/{{ entry.review().user_id().value() }}" class="feed-user">{{ entry.user_display_name() }}</a>
|
||||
<span class="feed-time">{{ entry.review().watched_at().format("%b %-d, %Y") }}</span>
|
||||
{% match entry.review().source() %}
|
||||
{% when ReviewSource::Remote with { actor_url } %}
|
||||
<span class="remote-badge" title="{{ actor_url }}">↗ federated</span>
|
||||
<a href="{{ actor_url }}" class="feed-user" target="_blank" rel="noopener noreferrer">{{ entry.user_display_name() }}</a>
|
||||
<span class="feed-time">{{ entry.review().watched_at().format("%b %-d, %Y") }}</span>
|
||||
<span class="remote-badge">↗ federated</span>
|
||||
{% when ReviewSource::Local %}
|
||||
<a href="/users/{{ entry.review().user_id().value() }}" class="feed-user">{{ entry.user_display_name() }}</a>
|
||||
<span class="feed-time">{{ entry.review().watched_at().format("%b %-d, %Y") }}</span>
|
||||
{% endmatch %}
|
||||
</div>
|
||||
{% if ctx.is_current_user(entry.review().user_id().value()) %}
|
||||
|
||||
@@ -10,15 +10,15 @@
|
||||
<div class="stat-label">movies</div>
|
||||
</div>
|
||||
<div class="stat-tile">
|
||||
<div class="stat-value">{{ stats.avg_rating_display() }}★</div>
|
||||
<div class="stat-value">{{ avg_rating_display }}★</div>
|
||||
<div class="stat-label">avg rating</div>
|
||||
</div>
|
||||
<div class="stat-tile">
|
||||
<div class="stat-value">{{ stats.favorite_director_display() }}</div>
|
||||
<div class="stat-value">{{ favorite_director_display }}</div>
|
||||
<div class="stat-label">fav director</div>
|
||||
</div>
|
||||
<div class="stat-tile">
|
||||
<div class="stat-value">{{ stats.most_active_month_display() }}</div>
|
||||
<div class="stat-value">{{ most_active_month_display }}</div>
|
||||
<div class="stat-label">most active</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,6 +36,27 @@
|
||||
{% endif %}
|
||||
</section>
|
||||
<a href="/users/{{ profile_user_id }}/following-list">View following ({{ following_count }})</a>
|
||||
{% if !pending_followers.is_empty() %}
|
||||
<section class="pending-followers">
|
||||
<h3>Pending follow requests ({{ pending_followers.len() }})</h3>
|
||||
<ul class="pending-list">
|
||||
{% for actor in pending_followers %}
|
||||
<li class="pending-item">
|
||||
<span class="pending-handle"><strong>{{ actor.handle }}</strong></span>
|
||||
<a href="{{ actor.url }}" class="pending-url" target="_blank" rel="noopener noreferrer">{{ actor.url }}</a>
|
||||
<form method="POST" action="/users/{{ profile_user_id }}/followers/accept" class="inline-form">
|
||||
<input type="hidden" name="actor_url" value="{{ actor.url }}">
|
||||
<button type="submit" class="btn-accept">Accept</button>
|
||||
</form>
|
||||
<form method="POST" action="/users/{{ profile_user_id }}/followers/reject" class="inline-form">
|
||||
<input type="hidden" name="actor_url" value="{{ actor.url }}">
|
||||
<button type="submit" class="btn-reject">Reject</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="view-tabs">
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
<p class="error">{{ err }}</p>
|
||||
{% endif %}
|
||||
<form method="POST" action="/register">
|
||||
<label>
|
||||
Username<br>
|
||||
<small>2–30 chars: letters, digits, underscores, hyphens. This becomes your ActivityPub handle.</small><br>
|
||||
<input type="text" name="username" required autocomplete="username"
|
||||
pattern="[a-z0-9_\-]{2,30}" title="2–30 lowercase letters, digits, underscores or hyphens">
|
||||
</label>
|
||||
<label>
|
||||
Email<br>
|
||||
<input type="email" name="email" required autocomplete="email">
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
<h2 class="page-title">Members</h2>
|
||||
{% for user in users %}
|
||||
<div class="user-row">
|
||||
<div class="user-avatar">{{ user.initial() }}</div>
|
||||
<div class="user-avatar">{{ user.initial }}</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name">{{ user.display_name() }}</div>
|
||||
<div class="user-meta">{{ user.total_movies }} movies · avg {{ user.avg_rating_display() }}★</div>
|
||||
<div class="user-name">{{ user.display_name }}</div>
|
||||
<div class="user-meta">{{ user.total_movies }} movies · avg {{ user.avg_rating_display }}★</div>
|
||||
</div>
|
||||
<a href="/users/{{ user.user_id.value() }}" class="btn-secondary">View profile →</a>
|
||||
<a href="/users/{{ user.user_id }}" class="btn-secondary">View profile →</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty">No users yet.</p>
|
||||
|
||||
Reference in New Issue
Block a user