diff --git a/.gitattributes b/.gitattributes index eb0c94757..7273afe43 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,10 +1,11 @@ *.ex diff=elixir *.exs diff=elixir -priv/static/instance/static.css diff=css - # Most of js/css files included in the repo are minified bundles, # and we don't want to search/diff those as text files. *.js binary *.js.map binary *.css binary + +priv/static/instance/static.css diff=css +priv/static/static-fe/static-fe.css diff=css diff --git a/CHANGELOG.md b/CHANGELOG.md index e0946b76d..07ed6653a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - NormalizeMarkup MRF is now on by default - Follow/Block/Mute imports now spin off into *n* tasks to avoid the oban timeout - Transient activities recieved from remote servers are no longer persisted in the database +- Overhauled static-fe view for logged-out users ## Upgrade Notes - If you have an old instance, you will probably want to run `mix pleroma.database prune_task` in the foreground to catch it up with the history of your instance. diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index a34dd26ce..22a35e8e2 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -728,6 +728,12 @@ defmodule Pleroma.Web.Router do get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed) end + scope "/", Pleroma.Web.StaticFE do + # Profile pages for static-fe + get("/users/:nickname/with_replies", StaticFEController, :show) + get("/users/:nickname/media", StaticFEController, :show) + end + scope "/", Pleroma.Web do pipe_through(:accepts_html) get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player) @@ -771,10 +777,16 @@ defmodule Pleroma.Web.Router do post("/users/:nickname/outbox", ActivityPubController, :update_outbox) post("/api/ap/upload_media", ActivityPubController, :upload_media) + get("/users/:nickname/collections/featured", ActivityPubController, :pinned) + end + + scope "/", Pleroma.Web.ActivityPub do + # Note: html format is supported only if static FE is enabled + pipe_through([:accepts_html_json, :static_fe, :activitypub_client]) + # The following two are S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`: get("/users/:nickname/followers", ActivityPubController, :followers) get("/users/:nickname/following", ActivityPubController, :following) - get("/users/:nickname/collections/featured", ActivityPubController, :pinned) end scope "/", Pleroma.Web.ActivityPub do diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex index 827c0a384..6f73b575e 100644 --- a/lib/pleroma/web/static_fe/static_fe_controller.ex +++ b/lib/pleroma/web/static_fe/static_fe_controller.ex @@ -45,7 +45,7 @@ def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do end end - def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do + def show(%{assigns: %{username_or_id: username_or_id, tab: tab}} = conn, params) do with {_, %User{local: true} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(username_or_id)}, {_, :visible} <- {:visibility, User.visible_for(user, _reading_user = nil)} do @@ -55,11 +55,36 @@ def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do params |> Map.take(@page_keys) |> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end) + |> Map.put(:limit, 20) + + params = + case tab do + "posts" -> + Map.put(params, :exclude_replies, true) + + "media" -> + Map.put(params, :only_media, true) + + _ -> + params + end timeline = - user - |> ActivityPub.fetch_user_activities(_reading_user = nil, params) - |> Enum.map(&represent/1) + case tab do + tab when tab in ["posts", "with_replies", "media"] -> + user + |> ActivityPub.fetch_user_activities(_reading_user = nil, params) + |> Enum.map(&represent/1) + + "following" when not user.hide_follows -> + User.get_friends(user) + + "followers" when not user.hide_followers -> + User.get_followers(user) + + _ -> + [] + end prev_page_id = (params["min_id"] || params["max_id"]) && @@ -75,6 +100,11 @@ def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do meta: meta }) else + {_, %User{} = user} -> + conn + |> put_status(:found) + |> redirect(external: user.uri || user.ap_id) + _ -> not_found(conn, "User not found.") end @@ -150,6 +180,23 @@ defp represent(%Activity{object: %Object{data: data}} = activity, selected) do nil end + reply_to_user = + if data["inReplyTo"] do + activity + |> Activity.get_in_reply_to_activity() + |> Map.get(:actor) + |> User.get_cached_by_ap_id() + else + nil + end + + total_votes = + if data["oneOf"] do + Enum.sum(for option <- data["oneOf"], do: option["replies"]["totalItems"]) + else + 0 + end + %{ user: User.sanitize_html(user), title: get_title(activity.object), @@ -160,7 +207,13 @@ defp represent(%Activity{object: %Object{data: data}} = activity, selected) do sensitive: data["sensitive"], selected: selected, counts: get_counts(activity), - id: activity.id + id: activity.id, + visibility: Visibility.get_visibility(activity.object), + reply_to: data["inReplyTo"], + reply_to_user: reply_to_user, + edited_at: data["updated"], + poll: data["oneOf"], + total_votes: total_votes } end @@ -177,7 +230,16 @@ defp assign_id(%{path_info: [_nickname, "status", notice_id]} = conn, _opts), do: assign(conn, :notice_id, notice_id) defp assign_id(%{path_info: ["users", user_id]} = conn, _opts), - do: assign(conn, :username_or_id, user_id) + do: + conn + |> assign(:username_or_id, user_id) + |> assign(:tab, "posts") + + defp assign_id(%{path_info: ["users", user_id, tab]} = conn, _opts), + do: + conn + |> assign(:username_or_id, user_id) + |> assign(:tab, tab) defp assign_id(%{path_info: ["objects", object_id]} = conn, _opts), do: assign(conn, :object_id, object_id) diff --git a/lib/pleroma/web/static_fe/static_fe_view.ex b/lib/pleroma/web/static_fe/static_fe_view.ex index c04715337..f0c9ddd22 100644 --- a/lib/pleroma/web/static_fe/static_fe_view.ex +++ b/lib/pleroma/web/static_fe/static_fe_view.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.StaticFE.StaticFEView do alias Calendar.Strftime alias Pleroma.Emoji.Formatter alias Pleroma.User - alias Pleroma.Web.Endpoint alias Pleroma.Web.Gettext alias Pleroma.Web.MediaProxy alias Pleroma.Web.Metadata.Utils @@ -22,17 +21,38 @@ def fetch_media_type(%{"mediaType" => mediaType}) do Utils.fetch_media_type(@media_types, mediaType) end + def time_ago(date) do + {:ok, date, _} = DateTime.from_iso8601(date) + now = DateTime.utc_now() + + Timex.from_now(date, now) + end + def format_date(date) do {:ok, date, _} = DateTime.from_iso8601(date) Strftime.strftime!(date, "%Y/%m/%d %l:%M:%S %p UTC") end - def instance_name, do: Pleroma.Config.get([:instance, :name], "Pleroma") + def instance_name, do: Pleroma.Config.get([:instance, :name], "Akkoma") def open_content? do Pleroma.Config.get( [:frontend_configurations, :collapse_message_with_subjects], - true + false ) end + + def get_attachment_name(%{"name" => name}), do: name + + def get_attachment_name(_), do: "" + + def poll_percentage(count, total_votes) do + case count do + 0 -> + "0%" + + _ -> + Integer.to_string(trunc(count / total_votes * 100)) <> "%" + end + end end diff --git a/lib/pleroma/web/templates/layout/static_fe.html.eex b/lib/pleroma/web/templates/layout/static_fe.html.eex index e6adb526b..3d55393f0 100644 --- a/lib/pleroma/web/templates/layout/static_fe.html.eex +++ b/lib/pleroma/web/templates/layout/static_fe.html.eex @@ -6,10 +6,39 @@ <%= Pleroma.Config.get([:instance, :name]) %> <%= Phoenix.HTML.raw(assigns[:meta] || "") %> + +
+
- <%= @inner_content %> +
+
+ <%= @inner_content %> +
+
+ + diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex index 4853e7f4b..f5bbe9a07 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex +++ b/lib/pleroma/web/templates/static_fe/static_fe/_attachment.html.eex @@ -1,8 +1,15 @@ -<%= case @mediaType do %> -<% "audio" -> %> - -<% "video" -> %> - -<% _ -> %> -<%= @name %> -<% end %> +" title="<%= @name %>"> + <%= if @nsfw do %> +
+
<%= gettext("Hover to show content") %>
+
+ <% end %> + <%= case @mediaType do %> + <% "audio" -> %> + + <% "video" -> %> + + <% _ -> %> + + <% end %> +
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex index df0244795..6585e81b6 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex +++ b/lib/pleroma/web/templates/static_fe/static_fe/_notice.html.eex @@ -1,41 +1,109 @@ -
id="selected" <% end %>> -

- - +

id="selected" <% end %>> +
+ +
+ <%= @user.nickname %> +
-

- <%= render("_user_card.html", %{user: @user}) %> -
- <%= if @title != "" do %> -
open<% end %>> - <%= raw @title %> -
<%= raw @content %>
-
- <% else %> -
<%= raw @content %>
- <% end %> - <%= for %{"name" => name, "url" => [url | _]} <- @attachment do %> - <%= if @sensitive do %> -
- <%= Gettext.gettext("sensitive media") %> -
- <%= render("_attachment.html", %{name: name, url: url["href"], - mediaType: fetch_media_type(url)}) %> -
-
- <% else %> - <%= render("_attachment.html", %{name: name, url: url["href"], - mediaType: fetch_media_type(url)}) %> - <% end %> - <% end %>
- <%= if @selected do %> -
-
<%= Gettext.gettext("replies") %>
<%= @counts.replies %>
-
<%= Gettext.gettext("announces") %>
<%= @counts.announces %>
-
<%= Gettext.gettext("likes") %>
<%= @counts.likes %>
-
- <% end %> +
+
+
+
+

+ <%= raw Formatter.emojify(@user.name, @user.emoji) %> +

+ +
+
+ + + + <%= if @visibility == "public" do %> + + <% else %> + <%= if @visibility == "unlisted" do %> + + <% end %> + <% end %> +
+
+ <%= if @reply_to do %> + + <% end %> + <%= if @edited_at do %> +
+ <%= gettext("Edited %{timeago}", timeago: time_ago(@edited_at)) %> +
+ <% end %> +
+
+ <%= if @title && @title != "" do %> + <%= raw @title %> +
open<% end %>> + <%= gettext("Show content") %> + <% end %> +
+ <%= raw @content %> + <%= if @poll && length(@poll) > 0 do %> +
+ <%= for %{"name" => option, "replies" => %{"totalItems" => count}} <- @poll do %> +
+ <%= poll_percentage(count, @total_votes) %> + <%= raw option %> +
+
+ <% end %> +
+ <% end %> + <%= if length(@attachment) > 0 do %> +
+ <%= for attachment = %{"url" => [url | _]} <- @attachment do %> + <%= render("_attachment.html", %{name: get_attachment_name(attachment), + url: url["href"], mediaType: fetch_media_type(url), nsfw: @sensitive}) %> + <% end %> +
+ <% end %> +
+ <%= if @title && @title != "" do %> +
+ <% end %> +
+ +
+
+ + <%= @counts.replies %> +
+
+ + <%= @counts.announces %> +
+
+ + <%= @counts.likes %> +
+
+
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex index 977b894d3..dc8717ea0 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex +++ b/lib/pleroma/web/templates/static_fe/static_fe/_user_card.html.eex @@ -1,11 +1,21 @@ -
- -
- +
+ +
+
+ <%= raw Formatter.emojify(@user.name, @user.emoji) %>
- - <%= raw Formatter.emojify(@user.name, @user.emoji) %> - <%= @user.nickname %> - - + +
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex index 2acd84828..b825c85e7 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex +++ b/lib/pleroma/web/templates/static_fe/static_fe/conversation.html.eex @@ -1,11 +1,8 @@ -
-

<%= link instance_name(), to: "/" %>

-
- -
-
- <%= for activity <- @activities do %> - <%= render("_notice.html", activity) %> - <% end %> +
+
+ <%= gettext("Conversation") %>
-
+ <%= for activity <- @activities do %> + <%= render("_notice.html", activity) %> + <% end %> +
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/error.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/error.html.eex index d98a1eba7..a9dbbf427 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/error.html.eex +++ b/lib/pleroma/web/templates/static_fe/static_fe/error.html.eex @@ -1,7 +1,8 @@ -
-

<%= gettext("Oops") %>

-
- -
-

<%= @message %>

-
+
+
+ <%= gettext("Error") %> +
+
+ <%= @message %> +
+
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex index a14ca305e..3d1cf77e5 100644 --- a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex +++ b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex @@ -1,31 +1,148 @@ -
-

<%= link instance_name(), to: "/" %>

- -

-
- - - -
- <%= raw Formatter.emojify(@user.name, @user.emoji) %> | - <%= link "@#{@user.nickname}@#{Endpoint.host()}", to: (@user.uri || @user.ap_id) %> -

-

<%= raw @user.bio %>

-
- -
-
- <%= for activity <- @timeline do %> - <%= render("_notice.html", Map.put(activity, :selected, false)) %> - <% end %> -

- <%= if @prev_page_id do %> - <%= link "«", to: "?min_id=" <> @prev_page_id %> - <% end %> - <%= if @prev_page_id && @next_page_id, do: " | " %> - <%= if @next_page_id do %> - <%= link "»", to: "?max_id=" <> @next_page_id %> - <% end %> -

+
+
+
+ +
+
+
<%= gettext("Posts") %>
+ <%= @user.note_count %> +
+
+
<%= gettext("Following") %>
+ <%= if @user.hide_follows_count do gettext("Hidden") else @user.following_count end %> +
+
+
<%= gettext("Followers") %>
+ <%= if @user.hide_followers_count do gettext("Hidden") else @user.follower_count end %> +
+
+ <%= raw Formatter.emojify(@user.bio, @user.emoji) %>
-
+ + + <%= if @prev_page_id do %> + <%= link gettext("Show newer"), to: "?min_id=" <> @prev_page_id, class: "load-posts" %> + <% end %> +
+ <%= if @tab in ["posts", "with_replies", "media"] do %> + <%= for activity <- @timeline do %> + <%= if(activity.user.id != @user.id) do %> +
+ + +
+ <% end %> + <%= render("_notice.html", Map.put(activity, :selected, false)) %> + <% end %> + <% else %> + <%= for user <- @timeline do %> + <%= render("_user_card.html", %{user: user}) %> + <% end %> + <% end %> +
+ <%= if @next_page_id do %> + <%= link gettext("Show older"), to: "?max_id=" <> @next_page_id, class: "load-posts" %> + <% end %> +
+ + diff --git a/lib/pleroma/web/views/layout_view.ex b/lib/pleroma/web/views/layout_view.ex index c2da10f04..ac13cf962 100644 --- a/lib/pleroma/web/views/layout_view.ex +++ b/lib/pleroma/web/views/layout_view.ex @@ -4,4 +4,11 @@ defmodule Pleroma.Web.LayoutView do use Pleroma.Web, :view + import Phoenix.HTML + + def render_html(file) do + case :httpc.request(Pleroma.Web.Endpoint.url() <> file) do + {:ok, {{_, 200, _}, _headers, body}} -> body + end + end end diff --git a/priv/static/static-fe/static-fe.css b/priv/static/static-fe/static-fe.css index 89e9f4877..657556077 100644 --- a/priv/static/static-fe/static-fe.css +++ b/priv/static/static-fe/static-fe.css @@ -1,59 +1,278 @@ +/* pleroma-light and pleroma-dark themes from pleroma-fe */ +:root { + --icon-filter: invert(38%) sepia(11%) saturate(209%) hue-rotate(179deg) brightness(99%) contrast(89%); + --wallpaper: rgba(11, 16, 23, 1); + --alertNeutral: rgba(185, 185, 186, 0.5); + --alertNeutralText: rgba(255, 255, 255, 1); + --avatarShadow: 0px 1px 8px 0px rgba(0, 0, 0, 0.7); + --loadPostsSelected: rgba(23, 34, 46, 1); + --loadPostsSelectedText: rgba(185, 185, 186, 1); + --profileBg: rgba(7, 12, 17, 1); + --profileTint: rgba(15, 22, 30, 0.5); + --btnText: rgba(185, 185, 186, 1); + --btn: rgba(21, 30, 43, 1); + --btnShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1) , 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; + --btnHoverShadow: 0px 0px 1px 2px rgba(185, 185, 186, 0.4) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; + --lightText: rgba(236, 236, 236, 1); + --panelShadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.5) , 0px 4px 6px 3px rgba(0, 0, 0, 0.3); + --panelHeaderShadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4) , 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset; + --topBar: rgba(21, 30, 43, 1); + --topBarText: rgba(159, 159, 161, 1); + --topBarShadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.4) , 0px 2px 7px 0px rgba(0, 0, 0, 0.3); + --underlay: rgba(9, 14, 20, 0.6); + --background: rgba(15, 22, 30, 1); + --faint: rgba(185, 185, 186, 0.5); + --selectedPost: rgba(23, 34, 46, 1); + --link: rgba(226, 177, 136, 1); + --text: rgba(185, 185, 186, 1); + --border: rgba(26, 37, 53, 1); + --poll: rgba(99, 84, 72, 1); +} +@media (prefers-color-scheme: light) { + :root { + --icon-filter: invert(67%) sepia(7%) saturate(525%) hue-rotate(173deg) brightness(90%) contrast(92%);; + --wallpaper: rgba(248, 250, 252, 1); + --alertNeutral: rgba(48, 64, 85, 0.5); + --alertNeutralText: rgba(0, 0, 0, 1); + --avatarShadow: 0px 1px 8px 0px rgba(0, 0, 0, 0.7); + --loadPostsSelected: rgba(224, 233, 240, 1); + --loadPostsSelectedText: rgba(48, 64, 85, 1); + --profileBg: rgba(128, 137, 146, 1); + --profileTint: rgba(242, 246, 249, 0.5); + --btnText: rgba(48, 64, 85, 1); + --btn: rgba(214, 223, 237, 1); + --btnShadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.2) , 0px 1px 0px 0px rgba(255, 255, 255, 0.5) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; + --btnHoverShadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.2) , 0px 0px 1px 2px rgba(255, 195, 159, 1) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; + --lightText: rgba(11, 14, 19, 1); + --panelShadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5) , 0px 3px 6px 1px rgba(0, 0, 0, 0.2); + --panelHeaderShadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.5) inset, 0px 1px 3px 0px rgba(0, 0, 0, 0.3); + --topBar: rgba(214, 223, 237, 1); + --topBarText: rgba(48, 64, 85, 1); + --topBarShadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.6); + --underlay: rgba(93, 96, 134, 0.4); + --background: rgba(242, 246, 249, 1); + --faint: rgba(48, 64, 85, 0.5); + --selectedPost: rgba(224, 233, 240, 1); + --link: rgba(245, 91, 27, 1); + --text: rgba(48, 64, 85, 1); + --border: rgba(216, 230, 249, 1); + --poll: rgba(243, 184, 160, 1); + } +} + +html { + height: 100%; + overflow-y: auto; +} + body { - background-color: #282c37; + overflow: auto; + margin: 0; + height: 100%; font-family: sans-serif; - color: white; + color: var(--text); } -main { - margin: 50px auto; - max-width: 960px; - padding: 40px; - background-color: #313543; - border-radius: 4px; -} - -header { - margin: 50px auto; - max-width: 960px; - padding: 40px; - background-color: #313543; - border-radius: 4px; -} - -.activity { - border-radius: 4px; - padding: 1em; - padding-bottom: 2em; - margin-bottom: 1em; -} - -.avatar { - cursor: pointer; -} - -.avatar img { - float: left; - border-radius: 4px; - margin-right: 4px; -} - -.activity-content img, video, audio { - padding: 1em; - max-width: 800px; - max-height: 800px; -} - -#selected { - background-color: #1b2735; -} - -.counts dt, .counts dd { - float: left; - margin-left: 1em; +.background-image { + position: fixed; + height: 100%; + top: 3.5em; + z-index: -1000; + left: 0; + right: -20px; + background-size: cover; + background-repeat: no-repeat; + background-color: var(--wallpaper); + background-image: var(--background-image); + background-position: 50%; } a { - color: white; + text-decoration: none; + color: var(--link); +} + +nav { + position: sticky; + top: 0; + width: 100%; + height: 3.5em; + background-color: var(--topBar); + box-shadow: var(--topBarShadow); + z-index: 2000; +} + +.inner-nav { + padding: 0 1.2em; + margin: auto; + max-width: 1110px; +} + +.inner-nav a { + line-height: 3.5em; + color: var(--topBarText); +} + +.inner-nav img { + height: 28px; + vertical-align: middle; + padding-right: 5px +} + +body > .container { + display: grid; + grid-template-columns: minmax(25em, 45em) 25em; + grid-template-areas: "content sidebar"; + height: calc(100vh - 3.5em); + justify-content: center; +} + +.underlay { + grid-column-start: 1; + grid-column-end: span 2; + grid-row-start: 1; + grid-row-end: 1; + background-color: var(--underlay); + z-index: -1000; +} + +.column { + padding: 1em; + margin: -0.5em; +} + +.panel { + background-color: var(--background); + border-radius: 3px; + box-shadow: var(--panelShadow); +} + +.panel-heading { + background-color: var(--topBar); + font-size: 1.3em; + padding: 0.6em; + border-radius: 3px 3px 0 0; + box-shadow: var(--panelHeaderShadow); +} + +.about-content { + padding: 0.6em; +} + +.main { + grid-area: content; + position: relative; +} + +.sidebar { + grid-area: sidebar; + padding-left: 0.5em; +} + +.status-container, +.repeat-header, +.user-card { + display: flex; + padding: 0.75em; +} + +.left-side { + margin-right: 0.75em; +} + +.right-side { + flex: 1; + min-width: 0; +} + +.repeat-header { + padding: 0.4em 0.75em; + margin-bottom: -0.75em; +} + +.repeat-header .right-side { + color: var(--faint); +} +.repeat-header .u-photo { + height: 20px; + width: 20px; + margin-left: 28px; +} + +.status-heading { + margin-bottom: 0.5em; + line-height: 1.3; +} + +.status-heading a { + display: inline-block; + word-break: break-all; +} + +.heading-left { + display: flex; + flex: 1; + overflow: hidden; +} + +.heading-right { + display: flex; + align-items: center; +} + +.heading-name-row .account-name { + min-width: 1.6em; + margin-right: 0.4em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1 1 0; +} + +.heading-name-row .username, +.repeat-header .username { + white-space: nowrap; + overflow: hidden; + max-width: 85%; + font-weight: bold; + flex-shrink: 1; + margin: 0; + margin-right: 0.4em; + text-overflow: ellipsis; +} + +.heading-name-row { + display: flex; + justify-content: space-between; +} + +.heading-edited-row, +.heading-reply-row { + font-size: 0.85em; + margin-top: 0.2em; +} + +.reply-to-link { + color: var(--faint); +} +.reply-to-link:hover { + text-decoration: underline; +} + +#selected { + background-color: var(--selectedPost); +} + +.timeago { + color: var(--faint); +} + +#selected .timeago { + color: var(--text); +} + +.timeago :hover { + text-decoration: underline; } .h-card { @@ -69,116 +288,340 @@ header a:hover, .h-card a:hover { text-decoration: underline; } -.display-name { - padding-top: 4px; +.attachments { + margin-top: 0.5em; + flex-direction: row; + display: flex; + flex-wrap: nowrap; + align-content: stretch; + max-height: 24em; +} + +.attachment { + border: 1px solid var(--border); + border-radius: 3px; + display: flex; + flex-grow: 1; + justify-content: center; + position: relative; + min-width: 0; +} + +.attachment > * { + width: 100%; + object-fit: contain; +} + +.attachment:not(:last-child) { + margin-right: 0.5em; +} + +.nsfw-banner { + position: absolute; + height: 100%; + display: flex; + align-items: center; +} +.nsfw-banner div { + width: 100%; + text-align: center; +} + +.nsfw-banner:not(:hover) { + background-color: var(--background); +} +.nsfw-banner:hover div { + display: none; +} + +.poll-option { + position: relative; + display: flex; + margin: 0.75em 0.5em; + padding: 0.1em 0.25em; + word-break: break-word; + z-index: 1; +} +.poll-option .percentage { + width: 3.5em; + flex-shrink: 0; +} +.poll-option .fill { + height: 100%; + position: absolute; + background-color: var(--poll); + border-radius: 3px; + top: 0; + left: 0; + z-index: -1; +} + +.status-actions { + position: relative; + width: 100%; + display: flex; + margin-top: 0.75em; +} +.status-actions > * { + max-width: 4em; + flex: 1; + display: flex; +} + +.status-summary { display: block; + font-style: italic; + padding-bottom: 0.5em; + margin-bottom: 0.5em; + border-style: solid; + border-width: 0 0 1px 0; + border-color: var(--border, #222); +} + +summary { + text-align: center; + color: var(--link); + cursor: pointer; +} + +.status-body { + word-wrap: break-word; + word-break: break-word; + line-height: 1.4; +} + +.user-info { + padding: 0.5em 26px; +} + +.user-info .container { + padding: 18px 0 6px 0; + display: flex; + align-items: flex-start; + max-height: 56px; +} + +.user-info a { + color: var(--lightText); +} + +.user-info .avatar img { + height: 56px; + width: 56px; +} + +.avatar img { + border-radius: 3px; + box-shadow: var(--avatarShadow); +} + +.user-summary { + display: block; + margin-left: 0.6em; + text-align: left; text-overflow: ellipsis; - overflow: hidden; - color: white; + white-space: nowrap; + flex: 1 1 0; + z-index: 1; + line-height: 2em; + color: var(--lightText); } -/* keep emoji from being hilariously huge */ -.display-name img { - max-height: 1em; - max-width: 1em; +.button-default { + user-select: none; + color: var(--btnText); + background-color: var(--btn); + border: none; + border-radius: 4px; + box-shadow: var(--btnShadow); + font-size: 1em; + min-height: 2em; } -.display-name .nickname { - padding-top: 4px; +.button-default:hover { + box-shadow: var(--btnHoverShadow); + cursor: pointer; +} + +.user-bio { + text-align: center; display: block; -} - -.nickname:hover { - text-decoration: none; -} - -.pull-right { - float: right; -} - -.collapse { - margin: 0; - width: auto; -} - -h1 { + line-height: 1.3; + padding: 1em; margin: 0; } -h2 { - color: #9baec8; - font-weight: normal; - font-size: 20px; - margin-bottom: 40px; +.user-banner { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: linear-gradient(to bottom, var(--profileTint), var(--profileTint)), + var(--user-banner); + background-size: cover; + background-color: var(--profileBg); + -webkit-mask: linear-gradient(to top, white, transparent) bottom no-repeat, + linear-gradient(to top, white, white); + -webkit-mask-composite: xor; + -webkit-mask-size: 100% 60%; + z-index: -2; } -form { - width: 100%; +.user-header { + position: relative; + z-index: 1; } -input { +.user-role { + color: var(--alertNeutralText); + background-color: var(--alertNeutral); + margin: 0 0.35em; + padding: 0 0.25em; + border-radius: 2px; +} + +.user-profile-fields { + margin: 0 0.5em; +} + +.user-profile-field { + display: flex; + margin: 0.25em; + border: 1px solid var(--border, #222); + border-radius: 3px; + line-height: 1.3; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.user-profile-field dt { + padding: 0.5em 1.5em; box-sizing: border-box; - width: 100%; - padding: 10px; - margin-top: 20px; - background-color: rgba(0,0,0,.1); - color: white; - border: 0; - border-bottom: 2px solid #9baec8; - font-size: 14px; -} - -input:focus { - border-bottom: 2px solid #4b8ed8; -} - -input[type="checkbox"] { - width: auto; -} - -button { - box-sizing: border-box; - width: 100%; - color: white; - background-color: #419bdd; - border-radius: 4px; - border: none; - padding: 10px; - margin-top: 30px; - text-transform: uppercase; + flex: 0 1 30%; font-weight: 500; - font-size: 16px; + color: var(--lightText); + border-right: 1px solid var(--border); + text-align: right; } -.alert-danger { +.user-profile-field dd { + padding: 0.5em 1.5em; box-sizing: border-box; - width: 100%; - color: #D8000C; - background-color: #FFD2D2; - border-radius: 4px; - border: none; - padding: 10px; - margin-top: 20px; - font-weight: 500; - font-size: 16px; + flex: 1 1 30%; + margin: 0 0 0 0.25em; } -.alert-info { +.user-counts { + display: flex; + line-height: 1em; + padding: 0.5em 1.5em 0 1.5em; + text-align: center; + justify-content: space-between; + color: var(--lightText); + flex-wrap: wrap; +} + +.user-count { + flex: 1 0 auto; + padding: 0.5em 0; + margin: 0 0.5em; +} + +.user-count h5 { + font-size: 1em; + font-weight: bolder; + margin: 0 0 0.25em; +} + +.tab-switcher { + display: flex; + padding-top: 5px; + overflow-x: auto; + overflow-y: hidden; + border-bottom: 1px solid var(--border); +} + +.tab-switcher::before, +.tab-switcher::after { + flex: 1 1 auto; + content: ''; +} + +.tab { + flex: 0 0 auto; + padding: 6px 1em; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.tab.active { + background: transparent; +} + +.profile .status-container { + border-bottom: 1px solid var(--border); +} + +.bottom-line { + display: flex; +} + +.load-posts { + display: block; box-sizing: border-box; + height: 3.5em; + line-height: 3.5em; + padding: 0 1em; width: 100%; - color: #00529B; - background-color: #BDE5F8; - border-radius: 4px; - border: none; - padding: 10px; - margin-top: 20px; - font-weight: 500; - font-size: 16px; + text-align: center; } -img.emoji { - width: 32px; - height: 32px; - padding: 0; - vertical-align: middle; +.load-posts:hover { + background-color: var(--loadPostsSelected); + color: var(--loadPostsSelectedText); +} + + +.fa-icon { + height: 0.875em; + margin: 0 0.3em; + filter: var(--icon-filter); + align-self: center; +} + +.status-actions .fa-icon { + height: 1.1em; +} + +.reply-to-link .fa-icon { + transform: scale(-1, 1); +} + +@media (max-width: 800px) { + body > .container { + display: block; + } + + .column { + padding: 0; + margin: 0; + } + + .sidebar { + display: none; + } +} + +img:not(.u-photo, .fa-icon) { + width: 32px; + height: 32px; + padding: 0; + vertical-align: middle; +} + +.username img:not(.u-photo) { + width: 16px; + height: 16px; } diff --git a/priv/static/static-fe/svg/globe-solid.svg b/priv/static/static-fe/svg/globe-solid.svg new file mode 100644 index 000000000..06aa71e62 --- /dev/null +++ b/priv/static/static-fe/svg/globe-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/priv/static/static-fe/svg/lock-open-solid.svg b/priv/static/static-fe/svg/lock-open-solid.svg new file mode 100644 index 000000000..a122cc414 --- /dev/null +++ b/priv/static/static-fe/svg/lock-open-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/priv/static/static-fe/svg/lock-solid.svg b/priv/static/static-fe/svg/lock-solid.svg new file mode 100644 index 000000000..3fdea7a51 --- /dev/null +++ b/priv/static/static-fe/svg/lock-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/priv/static/static-fe/svg/reply-solid.svg b/priv/static/static-fe/svg/reply-solid.svg new file mode 100644 index 000000000..a798b7666 --- /dev/null +++ b/priv/static/static-fe/svg/reply-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/priv/static/static-fe/svg/retweet-solid.svg b/priv/static/static-fe/svg/retweet-solid.svg new file mode 100644 index 000000000..260976770 --- /dev/null +++ b/priv/static/static-fe/svg/retweet-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/priv/static/static-fe/svg/star-regular.svg b/priv/static/static-fe/svg/star-regular.svg new file mode 100644 index 000000000..71d15e95d --- /dev/null +++ b/priv/static/static-fe/svg/star-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/pleroma/web/static_fe/static_fe_controller_test.exs b/test/pleroma/web/static_fe/static_fe_controller_test.exs index 5752cffda..25ed6e193 100644 --- a/test/pleroma/web/static_fe/static_fe_controller_test.exs +++ b/test/pleroma/web/static_fe/static_fe_controller_test.exs @@ -6,6 +6,8 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do use Pleroma.Web.ConnCase alias Pleroma.Activity + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI @@ -42,8 +44,67 @@ test "profile does not include private messages", %{conn: conn, user: user} do html = html_response(conn, 200) - assert html =~ ">public<" - refute html =~ ">private<" + assert html =~ "\npublic\n" + refute html =~ "\nprivate\n" + end + + test "main page does not include replies", %{conn: conn, user: user} do + {:ok, op} = CommonAPI.post(user, %{status: "beep"}) + CommonAPI.post(user, %{status: "boop", in_reply_to_id: op}) + + conn = get(conn, "/users/#{user.nickname}") + + html = html_response(conn, 200) + + assert html =~ "\nbeep\n" + refute html =~ "\nboop\n" + end + + test "media page only includes posts with attachments", %{conn: conn, user: user} do + file = %Plug.Upload{ + content_type: "image/jpeg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, %{id: media_id}} = ActivityPub.upload(file, actor: user.ap_id) + + CommonAPI.post(user, %{status: "virgin text post"}) + CommonAPI.post(user, %{status: "chad post with attachment", media_ids: [media_id]}) + + conn = get(conn, "/users/#{user.nickname}/media") + + html = html_response(conn, 200) + + assert html =~ "\nchad post with attachment\n" + refute html =~ "\nvirgin text post\n" + end + + test "show follower list", %{conn: conn, user: user} do + follower = insert(:user) + CommonAPI.follow(follower, user) + + conn = get(conn, "/users/#{user.nickname}/followers") + + html = html_response(conn, 200) + + assert html =~ "user-card" + end + + test "don't show followers if hidden", %{conn: conn, user: user} do + follower = insert(:user) + CommonAPI.follow(follower, user) + + {:ok, user} = + user + |> User.update_changeset(%{hide_followers: true}) + |> User.update_and_set_cache() + + conn = get(conn, "/users/#{user.nickname}/followers") + + html = html_response(conn, 200) + + refute html =~ "user-card" end test "pagination", %{conn: conn, user: user} do @@ -53,10 +114,10 @@ test "pagination", %{conn: conn, user: user} do html = html_response(conn, 200) - assert html =~ ">test30<" - assert html =~ ">test11<" - refute html =~ ">test10<" - refute html =~ ">test1<" + assert html =~ "\ntest30\n" + assert html =~ "\ntest11\n" + refute html =~ "\ntest10\n" + refute html =~ "\ntest1\n" end test "pagination, page 2", %{conn: conn, user: user} do @@ -67,10 +128,10 @@ test "pagination, page 2", %{conn: conn, user: user} do html = html_response(conn, 200) - assert html =~ ">test1<" - assert html =~ ">test10<" - refute html =~ ">test20<" - refute html =~ ">test29<" + assert html =~ "\ntest1\n" + assert html =~ "\ntest10\n" + refute html =~ "\ntest20\n" + refute html =~ "\ntest29\n" end test "does not require authentication on non-federating instances", %{ @@ -104,7 +165,7 @@ test "single notice page", %{conn: conn, user: user} do conn = get(conn, "/notice/#{activity.id}") html = html_response(conn, 200) - assert html =~ "
" + assert html =~ "
" assert html =~ user.nickname assert html =~ "testing a thing!" end