[feature] Allow user to choose "gallery" style layout for web view of profile (#3917)

* [feature] Allow user to choose "gallery" style web layout

* find a bug and squish it up and all day long you'll have good luck

* just a sec

* [performance] reindex public timeline + tinker with query a bit

* fiddling

* should be good now

* last bit of finagling, i'm done now i prommy

* panic normally
This commit is contained in:
tobi 2025-03-26 16:59:39 +01:00 committed by GitHub
commit b6e481d63e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 2921 additions and 1171 deletions

View file

@ -135,12 +135,12 @@ html, body {
}
/* Make show more/less buttons more legible */
.status button, .status .button {
.button {
background-color: var(--almost-white);
color: var(--almost-black);
border: var(--dashed-border);
}
.status button:hover, .status .button:hover {
.button:hover {
background-color: var(--almost-black);
color: var(--almost-white);
border: var(--dashed-border);

View file

@ -130,12 +130,12 @@ html, body {
}
/* Make show more/less buttons more legible */
.status button, .status .button {
.button {
background-color: var(--almost-black);
color: var(--almost-white);
border: var(--dashed-border);
}
.status button:hover, .status .button:hover {
.button:hover {
background-color: var(--almost-white);
color: var(--almost-black);
border: var(--dashed-border);

View file

@ -0,0 +1,207 @@
/*
GoToSocial
Copyright (C) GoToSocial Authors admin@gotosocial.org
SPDX-License-Identifier: AGPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@import "photoswipe/dist/photoswipe.css";
@import "photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css";
@import "plyr/dist/plyr.css";
.media-wrapper {
height: 100%;
width: 100%;
box-sizing: border-box;
border: 0.15rem solid $gray1;
border-radius: $br;
position: relative;
overflow: hidden;
z-index: 2;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
details {
position: absolute;
height: 100%;
width: 100%;
&[open] summary {
height: auto;
width: auto;
margin: 1rem;
padding: 0;
.show, video, img {
display: none;
}
.eye.button .hide {
display: inline-block;
grid-column: 1 / span 3;
grid-row: 1 / span 2;
}
}
summary {
position: absolute;
height: 100%;
width: 100%;
z-index: 3;
overflow: hidden;
display: grid;
padding: 1rem;
grid-template-columns: 1fr auto 1fr;
grid-template-rows: 1fr 1fr;
grid-template-areas:
"eye sensitive ."
". sensitive .";
&::-webkit-details-marker {
display: none; /* Safari */
}
.eye.button {
grid-area: eye;
align-self: start;
justify-self: start;
margin: 0;
padding: 0.4rem;
.fa-fw {
line-height: $fa-fw;
}
.hide {
display: none;
}
}
.show.sensitive {
grid-area: sensitive;
align-self: center;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
.button {
cursor: pointer;
align-self: center;
}
}
video, img {
z-index: -1;
position: absolute;
height: calc(100% + 1.2rem);
width: calc(100% + 1.2rem);
top: -0.6rem;
left: -0.6rem;
filter: blur(1.2rem);
}
}
video.plyr-video, .plyr {
position: absolute;
height: 100%;
width: 100%;
object-fit: contain;
background: $gray1;
}
.unknown-attachment {
.placeholder {
width: 100%;
height: 100%;
padding: 0.8rem;
border: 0.2rem dashed $white2;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
color: $white2;
.placeholder-external-link {
align-self: end;
font-size: 2.5rem;
}
.placeholder-icon {
width: 100%;
font-size: 3.5rem;
text-align: center;
margin-top: auto;
}
.placeholder-link-to {
width: 100%;
text-align: center;
margin-bottom: auto;
}
}
}
}
}
.pswp__button--open-post-link {
display: flex;
align-items: center;
justify-content: center;
span > i {
background: $status-bg;
color: $fg;
border-radius: 25%;
}
}
.plyr--video {
flex-direction: column-reverse;
.plyr__video-wrapper {
position: relative;
}
.plyr__controls {
align-self: stretch;
position: initial;
padding: 0.1rem;
padding-top: 0.2rem;
}
.plyr__control {
box-shadow: none;
}
.plyr__control--overlaid {
top: calc(50% - 18px);
}
}
.pswp__content {
padding: 2rem;
.plyr {
max-height: 100%;
}
}

View file

@ -0,0 +1,343 @@
/*
GoToSocial
Copyright (C) GoToSocial Authors admin@gotosocial.org
SPDX-License-Identifier: AGPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
.profile .profile-header {
background: $profile-bg;
border-radius: $br;
overflow: hidden;
margin-bottom: 1rem;
.moved-to {
padding: 1rem;
text-align: center;
}
.header-image-wrapper {
position: relative;
padding-top: 33.33%; /* aspect-ratio 1/3 */
img {
position: absolute;
top: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
}
/*
Basic info container has the user's avatar, display- and username, and role
It's partially overlapped over the header image, by a negative margin-top.
*/
$avatar-size: 8.5rem;
$name-size: 3rem;
$username-size: 2rem;
$overlap: calc($avatar-size - $name-size - $username-size);
.basic-info {
position: relative;
display: grid;
box-sizing: border-box;
grid-template-columns: $avatar-size auto 1fr;
grid-template-rows: $overlap $name-size auto;
grid-template-areas:
"avatar . ."
"avatar namerole namerole"
"avatar namerole namerole";
margin: 1rem;
margin-top: calc(-1 * $overlap);
gap: 0 1rem;
.avatar-image-wrapper {
grid-area: avatar;
border: 0.2rem solid $avatar-border;
border-radius: $br;
/*
Wrapper always same
size + proportions no
matter image inside.
*/
height: $avatar-size;
width: $avatar-size;
.avatar {
/*
Fit 100% of the wrapper.
*/
height: 100%;
width: 100%;
/*
Normalize non-square images.
*/
object-fit: cover;
/*
Prevent image extending
beyond rounded borders.
*/
border-radius: $br-inner;
}
}
.namerole {
grid-area: namerole;
display: grid;
gap: 0 1rem;
box-sizing: border-box;
grid-template-columns: 1fr auto;
grid-template-rows: $name-size auto;
grid-template-areas:
"displayname displayname"
"username role";
.displayname {
grid-area: displayname;
line-height: $name-size;
font-size: 1.5rem;
font-weight: bold;
}
.bot-username-wrapper {
display: flex;
gap: 0.5rem;
grid-area: username;
align-items: center;
.bot-legend-wrapper {
display: flex;
gap: 0.25rem;
align-items: center;
background: $bg;
color: $fg;
border-radius: $br;
padding: 0.1rem 0.4rem 0.2rem 0.4rem;
font-variant: small-caps;
font-weight: bold;
cursor: default;
.bot-icon {
/*
FA icon is weirdly
aligned so tweak it
*/
margin-top: 0.25rem;
}
}
.username {
min-width: 0;
line-height: $username-size;
font-size: 1rem;
font-weight: bold;
color: $fg-accent;
user-select: all;
}
}
.role {
background: $bg;
color: $fg;
border: 0.13rem solid $bg;
grid-area: role;
align-self: center;
justify-self: start;
border-radius: $br;
padding: 0.3rem;
line-height: 1.1rem;
font-size: 0.9rem;
font-variant: small-caps;
font-weight: bold;
&.admin {
color: $role-admin;
border-color: $role-admin;
}
&.moderator {
color: $role-mod;
border-color: $role-mod;
}
}
}
}
}
.profile .about-user {
flex: 35 14rem;
border-radius: $br;
overflow: hidden;
.col-header {
margin-bottom: -0.25rem;
}
dt {
font-weight: bold;
}
.fields {
background: $profile-bg;
display: flex;
flex-direction: column;
padding: 0 0.5rem;
padding-top: 0.25rem;
.field {
padding: 0.25rem;
display: flex;
flex-direction: column;
border-bottom: 0.1rem solid $gray2;
> dt, > dd {
word-break: break-word;
}
&:first-child {
border-top: 0.1rem solid $gray2;
}
}
}
.bio {
background: $profile-bg;
padding: 1rem 0.75rem;
padding-bottom: 1.25rem;
}
.accountstats {
background: $bg-accent;
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
.stats-item {
display: flex;
dt {
width: 7rem;
}
}
}
}
/*
RSS icon isn't really part of the profile header exactly,
but also it sort of is, and we want it styled the same for
both microblog and gallery view anyway, so include it here.
*/
.rss-icon {
display: block;
margin: -0.25rem 0;
.fa {
font-size: 2rem;
object-fit: contain;
vertical-align: middle;
color: $orange2;
/*
Can't size a single-color background, so we use
a linear-gradient that's effectively white.
*/
background: linear-gradient(to right, $white1 100%, transparent 0) no-repeat center center;
background-size: 1.2rem 1.4rem;
/* light mode */
@media (prefers-color-scheme: light) {
background: linear-gradient(to right, $white 100%, transparent 0) no-repeat center center;
background-size: 1.2rem 1.4rem;
}
}
}
/*
Tablet-ish-kinda size.
*/
@media screen and (max-width: 750px) {
.profile .profile-header {
.basic-info {
grid-template-columns: auto 1fr;
grid-template-rows: $avatar-size $name-size auto;
grid-template-areas:
"avatar avatar"
"namerole namerole"
"namerole namerole";
/*
Make display name a bit smaller
so there's more chance of being
able to read everything.
*/
.namerole {
.displayname {
font-size: 1.2rem;
line-height: 2rem;
margin-top: 0.5rem;
}
}
}
}
}
/*
Phone-ish-kinda size.
*/
@media screen and (max-width: 500px) {
.profile
.profile-header
.basic-info
.namerole {
/*
Line up in smallest possible
horizontal space to avoid overflow.
*/
display: flex;
flex-direction: column;
gap: 0.5rem;
/*
Don't hug the right anymore
(good life advice in general).
*/
.role {
align-self: flex-start;
}
/*
Allow this to wrap in case
of a really skinny screen.
*/
.bot-username-wrapper {
flex-wrap: wrap;
}
}
}

View file

@ -0,0 +1,44 @@
/*
GoToSocial
Copyright (C) GoToSocial Authors admin@gotosocial.org
SPDX-License-Identifier: AGPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@import "./_media-wrapper.css";
.media {
grid-column: span 3;
display: grid;
grid-template-columns: 50% 50%;
grid-auto-rows: 10rem;
overflow: hidden;
&.single .media-wrapper {
grid-column: span 2;
}
&.odd .media-wrapper:first-child,
&.double .media-wrapper {
grid-row: span 2;
}
@media screen and (max-width: 42rem) {
.media-wrapper {
grid-column: span 2;
grid-row: span 2;
}
}
}

View file

@ -22,7 +22,7 @@
****************************************/
@import "modern-normalize/modern-normalize.css";
@import "./prism.css";
@import "./_prism.css";
/* noto-sans-regular - latin */
@font-face {

View file

@ -0,0 +1,108 @@
/*
GoToSocial
Copyright (C) GoToSocial Authors admin@gotosocial.org
SPDX-License-Identifier: AGPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@import "./_profile-header.css";
@import "./_media-wrapper.css";
.page {
/*
Profile gallery can be wider than default.
*/
grid-template-columns: 1fr min(95%, 65rem) 1fr;
}
.profile {
.about-user {
margin-bottom: 1rem;
.accountstats {
flex-direction: row;
justify-content: space-between;
.stats-item {
gap: 0.5rem;
width: 25%;
justify-content: space-around;
dt {
width: fit-content;
margin-left: auto;
}
dd {
margin-right: auto;
}
}
@media screen and (max-width: 750px) {
flex-direction: column;
.stats-item {
width: fit-content;
dt {
width: 7rem;
}
}
}
}
}
.media-galleries-wrapper {
display: flex;
flex-direction: column;
gap: 1rem;
min-width: 0%;
.media-gallery {
margin-top: 0.15rem;
margin-bottom: 0.15rem;
display: grid;
gap: 0.15rem;
/* Desktop-ish width, show 3 cols of media */
grid-template-columns: repeat(3, 1fr);
@media screen and (max-width: 55rem) {
/* Tablet-ish width, switch to 2 cols */
grid-template-columns: repeat(2, 1fr);
}
@media screen and (max-width: 36rem) {
/* Mobile-ish width, switch to 1 col */
grid-template-columns: repeat(1, 1fr);
}
.media-wrapper {
aspect-ratio: 4/3;
border: 0;
border-radius: 0;
background: $bg;
}
}
.backnextlinks {
display: flex;
justify-content: space-between;
.next {
margin-left: auto;
}
}
}
}

View file

@ -17,13 +17,14 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@import "./_profile-header.css";
.page {
/*
Profile page can be a little wider than default
page, since we're using a side-by-side column view.
*/
grid-template-columns: 1fr minmax(auto, 60rem) 1fr;
grid-template-columns: 1fr min(92%, 65rem) 1fr;
grid-template-columns: 1fr min(95%, 65rem) 1fr;
}
.profile .column-split {
@ -32,244 +33,6 @@
gap: 1rem;
}
.profile .profile-header {
background: $profile-bg;
border-radius: $br;
overflow: hidden;
margin-bottom: 1rem;
.moved-to {
padding: 1rem;
text-align: center;
}
.header-image-wrapper {
position: relative;
padding-top: 33.33%; /* aspect-ratio 1/3 */
img {
position: absolute;
top: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
}
/*
Basic info container has the user's avatar, display- and username, and role
It's partially overlapped over the header image, by a negative margin-top.
*/
$avatar-size: 8.5rem;
$name-size: 3rem;
$username-size: 2rem;
$overlap: calc($avatar-size - $name-size - $username-size);
.basic-info {
position: relative;
display: grid;
box-sizing: border-box;
grid-template-columns: $avatar-size auto 1fr;
grid-template-rows: $overlap $name-size auto;
grid-template-areas:
"avatar . ."
"avatar namerole namerole"
"avatar namerole namerole";
margin: 1rem;
margin-top: calc(-1 * $overlap);
gap: 0 1rem;
.avatar-image-wrapper {
grid-area: avatar;
border: 0.2rem solid $avatar-border;
border-radius: $br;
/*
Wrapper always same
size + proportions no
matter image inside.
*/
height: $avatar-size;
width: $avatar-size;
.avatar {
/*
Fit 100% of the wrapper.
*/
height: 100%;
width: 100%;
/*
Normalize non-square images.
*/
object-fit: cover;
/*
Prevent image extending
beyond rounded borders.
*/
border-radius: $br-inner;
}
}
.namerole {
grid-area: namerole;
display: grid;
gap: 0 1rem;
box-sizing: border-box;
grid-template-columns: 1fr auto;
grid-template-rows: $name-size auto;
grid-template-areas:
"displayname displayname"
"username role";
.displayname {
grid-area: displayname;
line-height: $name-size;
font-size: 1.5rem;
font-weight: bold;
}
.bot-username-wrapper {
display: flex;
gap: 0.5rem;
grid-area: username;
align-items: center;
.bot-legend-wrapper {
display: flex;
gap: 0.25rem;
align-items: center;
background: $bg;
color: $fg;
border-radius: $br;
padding: 0.1rem 0.4rem 0.2rem 0.4rem;
font-variant: small-caps;
font-weight: bold;
cursor: default;
.bot-icon {
/*
FA icon is weirdly
aligned so tweak it
*/
margin-top: 0.25rem;
}
}
.username {
min-width: 0;
line-height: $username-size;
font-size: 1rem;
font-weight: bold;
color: $fg-accent;
user-select: all;
}
}
.role {
background: $bg;
color: $fg;
border: 0.13rem solid $bg;
grid-area: role;
align-self: center;
justify-self: start;
border-radius: $br;
padding: 0.3rem;
line-height: 1.1rem;
font-size: 0.9rem;
font-variant: small-caps;
font-weight: bold;
&.admin {
color: $role-admin;
border-color: $role-admin;
}
&.moderator {
color: $role-mod;
border-color: $role-mod;
}
}
}
}
}
/*
Tablet-ish-kinda size.
*/
@media screen and (max-width: 750px) {
.profile .profile-header {
.basic-info {
grid-template-columns: auto 1fr;
grid-template-rows: $avatar-size $name-size auto;
grid-template-areas:
"avatar avatar"
"namerole namerole"
"namerole namerole";
/*
Make display name a bit smaller
so there's more chance of being
able to read everything.
*/
.namerole {
.displayname {
font-size: 1.2rem;
line-height: 2rem;
margin-top: 0.5rem;
}
}
}
}
}
/*
Phone-ish-kinda size.
*/
@media screen and (max-width: 500px) {
.profile
.profile-header
.basic-info
.namerole {
/*
Line up in smallest possible
horizontal space to avoid overflow.
*/
display: flex;
flex-direction: column;
gap: 0.5rem;
/*
Don't hug the right anymore
(good life advice in general).
*/
.role {
align-self: flex-start;
}
/*
Allow this to wrap in case
of a really skinny screen.
*/
.bot-username-wrapper {
flex-wrap: wrap;
}
}
}
.profile .statuses-wrapper {
flex: 65 25rem;
display: flex;
@ -283,29 +46,6 @@
flex-direction: column;
gap: 0.4rem;
.rss-icon {
display: block;
margin: -0.25rem 0;
.fa {
font-size: 2rem;
object-fit: contain;
vertical-align: middle;
color: $orange2;
/*
Can't size a single-color background, so we use
a linear-gradient that's effectively white.
*/
background: linear-gradient(to right, $white1 100%, transparent 0) no-repeat center center;
background-size: 1.2rem 1.4rem;
/* light mode */
@media (prefers-color-scheme: light) {
background: linear-gradient(to right, $white 100%, transparent 0) no-repeat center center;
background-size: 1.2rem 1.4rem;
}
}
}
.backnextlinks {
display: flex;
justify-content: space-between;
@ -315,55 +55,3 @@
}
}
}
.profile .about-user {
flex: 35 14rem;
border-radius: $br;
overflow: hidden;
.col-header {
margin-bottom: -0.25rem;
}
dt {
font-weight: bold;
}
.fields {
background: $profile-bg;
display: flex;
flex-direction: column;
padding: 0 0.5rem;
padding-top: 0.25rem;
.field {
padding: 0.25rem;
display: flex;
flex-direction: column;
border-bottom: 0.1rem solid $gray2;
> dt, > dd {
word-break: break-word;
}
&:first-child {
border-top: 0.1rem solid $gray2;
}
}
}
.bio {
background: $profile-bg;
padding: 1rem 0.75rem;
padding-bottom: 1.25rem;
}
.accountstats {
background: $bg-accent;
padding: 0.75rem;
display: grid;
grid-template-columns: auto 1fr;
gap: 0.25rem 1rem;
}
}

View file

@ -17,14 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@import "photoswipe/dist/photoswipe.css";
@import "photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css";
@import "plyr/dist/plyr.css";
main {
background: transparent;
grid-auto-rows: auto;
}
@import "./_status-media.css";
.status {
background: $status-bg;
@ -257,172 +250,6 @@ main {
}
}
.media {
grid-column: span 3;
display: grid;
grid-template-columns: 50% 50%;
grid-auto-rows: 10rem;
overflow: hidden;
.media-wrapper {
height: 100%;
width: 100%;
box-sizing: border-box;
border: 0.15rem solid $gray1;
border-radius: $br;
position: relative;
overflow: hidden;
z-index: 2;
details {
position: absolute;
height: 100%;
width: 100%;
&[open] summary {
height: auto;
width: auto;
margin: 1rem;
padding: 0;
.show, video, img {
display: none;
}
.eye.button .hide {
display: inline-block;
grid-column: 1 / span 3;
grid-row: 1 / span 2;
}
}
summary {
position: absolute;
height: 100%;
width: 100%;
z-index: 3;
overflow: hidden;
display: grid;
padding: 1rem;
grid-template-columns: 1fr auto 1fr;
grid-template-rows: 1fr 1fr;
grid-template-areas:
"eye sensitive ."
". sensitive .";
&::-webkit-details-marker {
display: none; /* Safari */
}
.eye.button {
grid-area: eye;
align-self: start;
justify-self: start;
margin: 0;
padding: 0.4rem;
.fa-fw {
line-height: $fa-fw;
}
.hide {
display: none;
}
}
.show.sensitive {
grid-area: sensitive;
align-self: center;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
.button {
cursor: pointer;
align-self: center;
}
}
video, img {
z-index: -1;
position: absolute;
height: calc(100% + 1.2rem);
width: calc(100% + 1.2rem);
top: -0.6rem;
left: -0.6rem;
filter: blur(1.2rem);
}
}
video.plyr-video, .plyr {
position: absolute;
height: 100%;
width: 100%;
object-fit: contain;
background: $gray1;
}
.unknown-attachment {
.placeholder {
width: 100%;
height: 100%;
padding: 0.8rem;
border: 0.2rem dashed $white2;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
color: $white2;
.placeholder-external-link {
align-self: end;
font-size: 2.5rem;
}
.placeholder-icon {
width: 100%;
font-size: 3.5rem;
text-align: center;
margin-top: auto;
}
.placeholder-link-to {
width: 100%;
text-align: center;
margin-bottom: auto;
}
}
}
}
}
&.single .media-wrapper {
grid-column: span 2;
}
&.odd .media-wrapper:first-child,
&.double .media-wrapper {
grid-row: span 2;
}
@media screen and (max-width: 42rem) {
.media-wrapper {
grid-column: span 2;
grid-row: span 2;
}
}
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.status-info {
background: $status-info-bg;
color: $fg-reduced;
@ -448,10 +275,6 @@ main {
gap: 0.4rem;
}
.stats-item.published-at {
text-decoration: underline;
}
.stats-item:not(.published-at):not(.edited-at) {
z-index: 1;
user-select: none;
@ -497,34 +320,3 @@ main {
}
}
}
.plyr--video {
flex-direction: column-reverse;
.plyr__video-wrapper {
position: relative;
}
.plyr__controls {
align-self: stretch;
position: initial;
padding: 0.1rem;
padding-top: 0.2rem;
}
.plyr__control {
box-shadow: none;
}
.plyr__control--overlaid {
top: calc(50% - 18px);
}
}
.pswp__content {
padding: 2rem;
.plyr {
max-height: 100%;
}
}

View file

@ -18,8 +18,8 @@
*/
.thread {
#tag-name {
/* Ensure ridiculous length tags get wrapped */
word-wrap: anywhere;
}
#tag-name {
/* Ensure ridiculous length tags get wrapped */
word-wrap: anywhere;
}
}

View file

@ -40,6 +40,9 @@ const lightbox = new PhotoswipeLightbox({
gallery: '.photoswipe-gallery',
children: '.photoswipe-slide',
pswpModule: Photoswipe,
// Bit darker than default 0.8.
bgOpacity: 0.9,
loop: false,
});
new PhotoswipeCaptionPlugin(lightbox, {
@ -71,7 +74,9 @@ lightbox.addFilter('itemData', (item) => {
}
},
width: parseInt(el.dataset.pswpWidth),
height: parseInt(el.dataset.pswpHeight)
height: parseInt(el.dataset.pswpHeight),
parentStatus: el.dataset.pswpParentStatus,
attachmentId: el.dataset.pswpAttachmentId,
};
}
return item;
@ -98,6 +103,26 @@ lightbox.on("close", function () {
}
});
lightbox.on('uiRegister', function() {
lightbox.pswp.ui.registerElement({
name: 'open-post-link',
ariaLabel: 'Open post',
order: 8,
isButton: true,
tagName: "a",
html: '<span title="Open post"><span class="sr-only">Open post</span><i class="fa fa-lg fa-external-link-square" aria-hidden="true"></i></span>',
onInit: (el, pswp) => {
el.setAttribute('target', '_blank');
el.setAttribute('rel', 'noopener');
pswp.on('change', () => {
el.href = pswp.currSlide.data.parentStatus
? pswp.currSlide.data.parentStatus
: pswp.currSlide.data.element.dataset.pswpParentStatus;
});
}
});
});
lightbox.init();
function dynamicSpoiler(className, updateFunc) {
@ -156,22 +181,40 @@ Array.from(document.getElementsByClassName("plyr-video")).forEach((video) => {
let player = new Plyr(video, {
title: video.title,
settings: ["loop"],
settings: [],
controls: ['play-large', 'play', 'progress', 'current-time', 'volume', 'mute', 'fullscreen'],
disableContextMenu: false,
hideControls: false,
tooltips: { contrors: true, seek: true },
tooltips: { controls: true, seek: true },
iconUrl: "/assets/plyr.svg",
invertTime: false,
listeners: {
fullscreen: () => {
if (player.playing) {
setTimeout(() => {
player.play();
}, 1);
// Check if the photoswipe lightbox is
// open with this as the current slide.
const alreadyInLightbox = (
lightbox.pswp !== undefined &&
video.dataset.pswpAttachmentId === lightbox.pswp.currSlide.data.attachmentId
);
if (alreadyInLightbox) {
// If this video is already open as the
// current photoswipe slide, the fullscreen
// button toggles proper fullscreen.
player.fullscreen.toggle();
} else {
// Otherwise the fullscreen button opens
// the video as current photoswipe slide.
//
// (Don't pause the video while it's
// being transitioned to a slide.)
if (player.playing) {
setTimeout(() => player.play(), 1);
}
lightbox.loadAndOpen(parseInt(video.dataset.pswpIndex), {
gallery: video.closest(".photoswipe-gallery")
});
}
lightbox.loadAndOpen(parseInt(video.dataset.pswpIndex), {
gallery: video.closest(".photoswipe-gallery")
});
return false;
}
}

View file

@ -24,7 +24,7 @@
"object-to-formdata": "^4.4.2",
"papaparse": "^5.3.2",
"parse-link-header": "^2.0.0",
"photoswipe": "^5.3.3",
"photoswipe": "^5.4.4",
"photoswipe-dynamic-caption-plugin": "^1.2.7",
"plyr": "^3.7.8",
"psl": "^1.9.0",

View file

@ -79,6 +79,8 @@ export interface AccountSource {
privacy: string;
sensitive: boolean;
status_content_type: string;
web_visibility: string;
web_layout: string;
}
export interface SearchAccountParams {

View file

@ -61,20 +61,6 @@ interface UserProfileFormProps {
}
function UserProfileForm({ data: profile }: UserProfileFormProps) {
/*
User profile update form keys
- bool bot
- bool locked
- string display_name
- string note
- file avatar
- file header
- bool enable_rss
- bool hide_collections
- string custom_css (if enabled)
- string theme
*/
const { data: instance } = useInstanceV1Query();
const instanceConfig = React.useMemo(() => {
return {
@ -120,7 +106,8 @@ function UserProfileForm({ data: profile }: UserProfileFormProps) {
discoverable: useBoolInput("discoverable", { source: profile}),
enableRSS: useBoolInput("enable_rss", { source: profile }),
hideCollections: useBoolInput("hide_collections", { source: profile }),
webVisibility: useTextInput("web_visibility", { source: profile, valueSelector: (p) => p.source?.web_visibility }),
webVisibility: useTextInput("web_visibility", { source: profile, valueSelector: (p: Account) => p.source?.web_visibility }),
webLayout: useTextInput("web_layout", { source: profile, valueSelector: (p: Account) => p.source?.web_layout }),
fields: useFieldArrayInput("fields_attributes", {
defaultValue: profile?.source?.fields,
length: instanceConfig.maxPinnedFields
@ -185,18 +172,24 @@ function UserProfileForm({ data: profile }: UserProfileFormProps) {
/>
</fieldset>
<div className="theme">
<div>
<b id="theme-label">Theme</b>
<br/>
<span>After choosing theme and saving, <a href={profile.url} target="_blank">open your profile</a> and refresh to see changes.</span>
</div>
<Select
aria-labelledby="theme-label"
field={form.theme}
options={<>{themeOptions}</>}
/>
</div>
<span>After choosing theme or layout and saving, <a href={profile.url} target="_blank">open your profile</a> and refresh to see changes.</span>
<Select
label="Theme for the web view of your profile"
field={form.theme}
options={<>{themeOptions}</>}
/>
<Select
field={form.webLayout}
label="Layout for the web view of your profile"
options={
<>
<option value="microblog">Classic microblog layout (show recent + pinned posts; media shown alongside its parent post)</option>
<option value="gallery">'Gram-style gallery layout (show recent + pinned media; parent posts still accessible by link)</option>
</>
}
/>
</div>
<div className="form-section-docs">

View file

@ -5399,10 +5399,10 @@ photoswipe-dynamic-caption-plugin@^1.2.7:
resolved "https://registry.yarnpkg.com/photoswipe-dynamic-caption-plugin/-/photoswipe-dynamic-caption-plugin-1.2.7.tgz#53aa5059f1c4dccc8aa36196ff3e09baa5e537c2"
integrity sha512-5XXdXLf2381nwe7KqQvcyStiUBi9TitYXppUQTrzPwYAi4lZsmWNnNKMclM7I4QGlX6fXo42v3bgb6rlK9pY1Q==
photoswipe@^5.3.3:
version "5.4.2"
resolved "https://registry.yarnpkg.com/photoswipe/-/photoswipe-5.4.2.tgz#bed976c27f876bd9c86085a022701a8cea484f7e"
integrity sha512-z5hr36nAIPOZbHJPbCJ/mQ3+ZlizttF9za5gKXKH/us1k4KNHaRbC63K1Px5sVVKUtGb/2+ixHpKqtwl0WAwvA==
photoswipe@^5.4.4:
version "5.4.4"
resolved "https://registry.yarnpkg.com/photoswipe/-/photoswipe-5.4.4.tgz#e045dc036453493188d5c8665b0e8f1000ac4d6e"
integrity sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==
picocolors@^1.0.0:
version "1.0.0"

View file

@ -0,0 +1,87 @@
{{- /*
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ -}}
{{- with . }}
<main class="profile h-card">
{{- with . }}
{{- include "profile_header.tmpl" . | indent 1 }}
{{- end }}
{{- with . }}
{{- include "profile_about_user.tmpl" . | indent 1 }}
{{- end }}
<div
class="media-galleries-wrapper"
role="region"
aria-label="Media by {{ .account.Username -}}"
>
{{- if .pinned_statuses }}
<section class="pinned h-feed" aria-labelledby="pinned">
<div class="col-header">
<h3 class="p-name" id="pinned">Pinned media</h3>
<a href="#recent">jump to recent</a>
</div>
<div
class="media-gallery photoswipe-gallery"
role="group"
>
{{- range $index, $attachment := .pinnedGalleryItems }}
{{- includeIndex "status_attachment.tmpl" $attachment $index | indent 4 }}
{{- end }}
</div>
</section>
{{- end }}
<section class="recent h-feed" aria-labelledby="recent">
<div class="col-header">
<h3 id="recent p-name" tabindex="-1">Recent media</h3>
{{- if .rssFeed }}
<a href="{{- .rssFeed -}}" class="rss-icon" aria-label="RSS feed">
<i class="fa fa-rss-square" aria-hidden="true"></i>
</a>
{{- end }}
</div>
{{- if not .galleryItems }}
<div data-nosnippet class="nothinghere">
{{- if .show_back_to_top }}
Reached the end of visible media!
{{- else }}
Nothing to see here! {{ .account.Username }} has either not posted any public media yet, or has opted not to make posts visible via the World Wide Web.
{{- end }}
</div>
{{- else }}
<div
class="media-gallery photoswipe-gallery"
role="group"
>
{{- range $index, $attachment := .galleryItems }}
{{- includeIndex "status_attachment.tmpl" $attachment $index | indent 4 }}
{{- end }}
</div>
{{- end }}
<nav class="backnextlinks">
{{- if .show_back_to_top }}
<a href="/@{{- .account.Username -}}">Back to top</a>
{{- end }}
{{- if .statuses_next }}
<a href="{{- .statuses_next -}}" class="next">Show older</a>
{{- end }}
</nav>
</section>
</div>
</main>
{{- end }}

View file

@ -17,200 +17,15 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ -}}
{{- define "profileMovedTo" -}}
{{- with .account.Moved }}
<div class="moved-to">
<b>
This account has permanently moved to
<a
href="{{ .URL }}"
class="nounderline"
rel="nofollow noreferrer noopener"
target="_blank"
>
@{{ .Username }}
</a>
</b>
</div>
{{- end }}
{{- end -}}
{{- define "defaultAvatarDimension" -}}
{{- /* 136 is the default width/height for 8.5rem avatars, double it to get a good look when expanded. */ -}}
272
{{- end -}}
{{- define "avatarWidth" -}}
{{- with .account }}
{{- if isNil .AvatarAttachment -}}
{{- template "defaultAvatarDimension" . -}}
{{- else -}}
{{- /* Use the avatar's proper dimensions. */ -}}
{{- .AvatarAttachment.Meta.Original.Width -}}
{{- end -}}
{{- end }}
{{- end -}}
{{- define "avatarHeight" -}}
{{- with .account }}
{{- if isNil .AvatarAttachment -}}
{{- template "defaultAvatarDimension" . -}}
{{- else -}}
{{- /* Use the avatar's proper dimensions. */ -}}
{{- .AvatarAttachment.Meta.Original.Height -}}
{{- end -}}
{{- end }}
{{- end -}}
{{- define "avatarAlt" -}}
Avatar for {{ .account.Username -}}
{{- if .account.AvatarDescription }}
{{- /* Add the avatar's image description. */ -}}
: {{ .account.AvatarDescription -}}
{{- end -}}
{{- end -}}
{{- define "headerAlt" -}}
Header for {{ .account.Username -}}
{{- if .account.HeaderDescription }}
{{- /* Add the header's image description. */ -}}
: {{ .account.HeaderDescription -}}
{{- end -}}
{{- end -}}
{{- define "avatar" -}}
{{- with . }}
<div
class="media photoswipe-gallery odd single avatar-image-wrapper"
role="group"
>
<a
class="photoswipe-slide"
href="{{- .account.Avatar -}}"
target="_blank"
data-pswp-width="{{- template "avatarWidth" . -}}px"
data-pswp-height="{{- template "avatarHeight" . -}}px"
data-cropped="true"
alt="{{- template "avatarAlt" . -}}"
title="{{- template "avatarAlt" . -}}"
>
<picture
aria-hidden="true"
>
{{- if .account.AvatarAttachment }}
<source
class="avatar"
srcset="{{- .account.AvatarStatic -}}"
type="{{- .account.AvatarAttachment.PreviewMIMEType -}}"
media="(prefers-reduced-motion: reduce)"
/>
{{- end }}
<img
class="avatar u-photo"
src="{{- .account.Avatar -}}"
alt="{{- template "avatarAlt" . -}}"
title="{{- template "avatarAlt" . -}}"
width="{{- template "avatarWidth" . -}}"
height="{{- template "avatarHeight" . -}}"
/>
</picture>
</a>
</div>
{{- end }}
{{- end -}}
{{- with . }}
<main class="profile h-card">
<h2 class="sr-only">Profile for {{ .account.Username -}}</h2>
<section class="profile-header" role="region" aria-label="Basic info">
{{- if .account.Moved }}
{{- include "profileMovedTo" . | indent 2 }}
{{- end }}
<div class="header-image-wrapper">
<picture>
{{- if .account.HeaderAttachment }}
<source
srcset="{{- .account.HeaderStatic -}}"
type="{{- .account.HeaderAttachment.PreviewMIMEType -}}"
media="(prefers-reduced-motion: reduce)"
/>
{{- end }}
<img
src="{{- .account.Header -}}"
alt="{{- template "headerAlt" . -}}"
title="{{- template "headerAlt" . -}}"
/>
</picture>
</div>
<div class="basic-info">
{{- with . }}
{{- include "avatar" . | indent 3 }}
{{- end }}
<dl class="namerole">
<dt class="sr-only">Display name</dt>
<dd class="displayname text-cutoff p-name">
{{- if .account.DisplayName -}}
{{- emojify .account.Emojis (escape .account.DisplayName) -}}
{{- else -}}
{{- .account.Username -}}
{{- end -}}
</dd>
<div class="bot-username-wrapper">
{{- if .account.Bot }}
<dt class="sr-only">Bot account</dt>
<dd>
<span class="sr-only">true</span>
<div
class="bot-legend-wrapper"
aria-hidden="true"
title="This is a bot account."
>
<i class="bot-icon fa fa-microchip"></i>
<span class="bot-legend">bot</span>
</div>
</dd>
{{- end }}
<dt class="sr-only">Username</dt>
<dd class="username text-cutoff p-nickname">@{{- .account.Username -}}@{{- .instance.AccountDomain -}}</dd>
</div>
{{- if .account.Roles }}
<dt class="sr-only">Role</dt>
{{- range .account.Roles }}
<dd class="role {{ .Name -}}">{{- .Name -}}</dd>
{{- end }}
{{- end }}
</dl>
<a class="u-url u-uid hidden" rel="me" href="/@{{- .account.Username -}}"></a>
</div>
</section>
{{- with . }}
{{- include "profile_header.tmpl" . | indent 1 }}
{{- end }}
<div class="column-split">
<section class="about-user" role="region" aria-labelledby="about-header">
<div class="col-header">
<h3 id="about-header">About<span class="sr-only">&nbsp;{{- .account.Username -}}</span></h3>
</div>
{{- if .account.Fields }}
{{- include "profile_fields.tmpl" . | indent 3 }}
{{- end }}
<h4 class="sr-only">Bio</h4>
<div class="bio p-note">
{{- if .account.Note }}
{{ emojify .account.Emojis (noescape .account.Note) }}
{{- else }}
<p>This GoToSocial user hasn't written a bio yet!</p>
{{- end }}
</div>
<h4 class="sr-only">Stats</h4>
<dl class="accountstats">
<dt>Joined</dt>
<dd><time datetime="{{- .account.CreatedAt -}}">{{- .account.CreatedAt | timestampVague -}}</time></dd>
<dt>Posts</dt>
<dd>{{- .account.StatusesCount -}}</dd>
<dt>Followed by</dt>
<dd>{{- if .account.HideCollections -}}<i>hidden</i>{{- else -}}{{- .account.FollowersCount -}}{{- end -}}</dd>
<dt>Following</dt>
<dd>{{- if .account.HideCollections -}}<i>hidden</i>{{- else -}}{{- .account.FollowingCount -}}{{- end -}}</dd>
</dl>
</section>
{{- with . }}
{{- include "profile_about_user.tmpl" . | indent 2 }}
{{- end }}
<div class="statuses-wrapper" role="region" aria-label="Posts by {{ .account.Username -}}">
{{- if .pinned_statuses }}
<section class="pinned statuses h-feed" aria-labelledby="pinned">

View file

@ -0,0 +1,56 @@
{{- /*
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ -}}
{{- with . }}
<section class="about-user" role="region" aria-labelledby="about-header">
<div class="col-header">
<h3 id="about-header">About<span class="sr-only">&nbsp;{{- .account.Username -}}</span></h3>
</div>
{{- if .account.Fields }}
{{- include "profile_fields.tmpl" . | indent 1 }}
{{- end }}
<h4 class="sr-only">Bio</h4>
<div class="bio p-note">
{{- if .account.Note }}
{{ emojify .account.Emojis (noescape .account.Note) }}
{{- else }}
<p>This GoToSocial user hasn't written a bio yet!</p>
{{- end }}
</div>
<h4 class="sr-only">Stats</h4>
<dl class="accountstats">
<div class="stats-item">
<dt class="joineddt text-cutoff">Joined</dt>
<dd class="joineddd text-cutoff"><time datetime="{{- .account.CreatedAt -}}">{{- .account.CreatedAt | timestampVague -}}</time></dd>
</div>
<div class="stats-item">
<dt class="postsdt text-cutoff">Posts</dt>
<dd class="postsdd text-cutoff">{{- .account.StatusesCount -}}</dd>
</div>
<div class="stats-item">
<dt class="followeddt text-cutoff">Followed by</dt>
<dd class="followeddd text-cutoff">{{- if .account.HideCollections -}}<i>hidden</i>{{- else -}}{{- .account.FollowersCount -}}{{- end -}}</dd>
</div>
<div class="stats-item">
<dt class="followingdt text-cutoff">Following</dt>
<dd class="followingdd text-cutoff">{{- if .account.HideCollections -}}<i>hidden</i>{{- else -}}{{- .account.FollowingCount -}}{{- end -}}</dd>
</div>
</dl>
</section>
{{- end }}

View file

@ -0,0 +1,185 @@
{{- /*
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ -}}
{{- define "profileMovedTo" -}}
{{- with .account.Moved }}
<div class="moved-to">
<b>
This account has permanently moved to
<a
href="{{ .URL }}"
class="nounderline"
rel="nofollow noreferrer noopener"
target="_blank"
>
@{{ .Username }}
</a>
</b>
</div>
{{- end }}
{{- end -}}
{{- define "defaultAvatarDimension" -}}
{{- /* 136 is the default width/height for 8.5rem avatars, double it to get a good look when expanded. */ -}}
272
{{- end -}}
{{- define "avatarWidth" -}}
{{- with .account }}
{{- if isNil .AvatarAttachment -}}
{{- template "defaultAvatarDimension" . -}}
{{- else -}}
{{- /* Use the avatar's proper dimensions. */ -}}
{{- .AvatarAttachment.Meta.Original.Width -}}
{{- end -}}
{{- end }}
{{- end -}}
{{- define "avatarHeight" -}}
{{- with .account }}
{{- if isNil .AvatarAttachment -}}
{{- template "defaultAvatarDimension" . -}}
{{- else -}}
{{- /* Use the avatar's proper dimensions. */ -}}
{{- .AvatarAttachment.Meta.Original.Height -}}
{{- end -}}
{{- end }}
{{- end -}}
{{- define "avatarAlt" -}}
Avatar for {{ .account.Username -}}
{{- if .account.AvatarDescription }}
{{- /* Add the avatar's image description. */ -}}
: {{ .account.AvatarDescription -}}
{{- end -}}
{{- end -}}
{{- define "headerAlt" -}}
Header for {{ .account.Username -}}
{{- if .account.HeaderDescription }}
{{- /* Add the header's image description. */ -}}
: {{ .account.HeaderDescription -}}
{{- end -}}
{{- end -}}
{{- define "avatar" -}}
{{- with . }}
<div
class="photoswipe-gallery odd single avatar-image-wrapper"
role="group"
>
<a
class="photoswipe-slide"
href="{{- .account.Avatar -}}"
target="_blank"
data-pswp-width="{{- template "avatarWidth" . -}}px"
data-pswp-height="{{- template "avatarHeight" . -}}px"
data-cropped="true"
alt="{{- template "avatarAlt" . -}}"
title="{{- template "avatarAlt" . -}}"
>
<picture
aria-hidden="true"
>
{{- if .account.AvatarAttachment }}
<source
class="avatar"
srcset="{{- .account.AvatarStatic -}}"
type="{{- .account.AvatarAttachment.PreviewMIMEType -}}"
media="(prefers-reduced-motion: reduce)"
/>
{{- end }}
<img
class="avatar u-photo"
src="{{- .account.Avatar -}}"
alt="{{- template "avatarAlt" . -}}"
title="{{- template "avatarAlt" . -}}"
width="{{- template "avatarWidth" . -}}"
height="{{- template "avatarHeight" . -}}"
/>
</picture>
</a>
</div>
{{- end }}
{{- end -}}
{{- with . }}
<h2 class="sr-only">Profile for {{ .account.Username -}}</h2>
<section class="profile-header" role="region" aria-label="Basic info">
{{- if .account.Moved }}
{{- include "profileMovedTo" . | indent 2 }}
{{- end }}
<div class="header-image-wrapper">
<picture>
{{- if .account.HeaderAttachment }}
<source
srcset="{{- .account.HeaderStatic -}}"
type="{{- .account.HeaderAttachment.PreviewMIMEType -}}"
media="(prefers-reduced-motion: reduce)"
/>
{{- end }}
<img
src="{{- .account.Header -}}"
alt="{{- template "headerAlt" . -}}"
title="{{- template "headerAlt" . -}}"
/>
</picture>
</div>
<div class="basic-info">
{{- with . }}
{{- include "avatar" . | indent 3 }}
{{- end }}
<dl class="namerole">
<dt class="sr-only">Display name</dt>
<dd class="displayname text-cutoff p-name">
{{- if .account.DisplayName -}}
{{- emojify .account.Emojis (escape .account.DisplayName) -}}
{{- else -}}
{{- .account.Username -}}
{{- end -}}
</dd>
<div class="bot-username-wrapper">
{{- if .account.Bot }}
<dt class="sr-only">Bot account</dt>
<dd>
<span class="sr-only">true</span>
<div
class="bot-legend-wrapper"
aria-hidden="true"
title="This is a bot account."
>
<i class="bot-icon fa fa-microchip"></i>
<span class="bot-legend">bot</span>
</div>
</dd>
{{- end }}
<dt class="sr-only">Username</dt>
<dd class="username text-cutoff p-nickname">@{{- .account.Username -}}@{{- .instance.AccountDomain -}}</dd>
</div>
{{- if .account.Roles }}
<dt class="sr-only">Role</dt>
{{- range .account.Roles }}
<dd class="role {{ .Name -}}">{{- .Name -}}</dd>
{{- end }}
{{- end }}
</dl>
<a class="u-url u-uid hidden" rel="me" href="/@{{- .account.Username -}}"></a>
</div>
</section>
{{- end }}

View file

@ -30,6 +30,16 @@
it in an appropriate <article></article>!
*/ -}}
{{- /* Produces something like "1 attachment", "2 attachments", etc */ -}}
{{- define "attachmentsLength" -}}
{{- (len .) }}{{- if eq (len .) 1 }} attachment{{- else }} attachments{{- end -}}
{{- end -}}
{{- /* Produces something like "media photoswipe-gallery odd single" */ -}}
{{- define "galleryClass" -}}
media photoswipe-gallery {{ (len .) | oddOrEven }} {{ if eq (len .) 1 }}single{{ else if eq (len .) 2 }}double{{ end }}
{{- end -}}
{{- with . }}
<header class="status-header">
{{- include "status_header.tmpl" . | indent 1 }}
@ -63,7 +73,15 @@
</div>
{{- end }}
{{- if .MediaAttachments }}
{{- include "status_attachments.tmpl" . | indent 1 }}
<div
class="{{- template "galleryClass" .MediaAttachments -}}"
role="group"
aria-label="{{- template "attachmentsLength" .MediaAttachments -}}"
>
{{- range $index, $media := .MediaAttachments }}
{{- includeIndex "status_attachment.tmpl" $media $index | indent 2 }}
{{- end }}
</div>
{{- end }}
</div>
<aside class="status-info" aria-hidden="true">

View file

@ -0,0 +1,179 @@
{{- /*
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ -}}
{{- define "imagePreview" }}
<img
src="{{- .PreviewURL -}}"
loading="lazy"
{{- if .Description }}
alt="{{- .Description -}}"
title="{{- .Description -}}"
{{- end }}
width="{{- .Meta.Original.Width -}}"
height="{{- .Meta.Original.Height -}}"
/>
{{- end }}
{{- define "videoPreview" }}
<img
src="{{- .PreviewURL -}}"
loading="lazy"
{{- if .Description }}
alt="{{- .Description -}}"
title="{{- .Description -}}"
{{- end }}
width="{{- .Meta.Small.Width -}}"
height="{{- .Meta.Small.Height -}}"
/>
{{- end }}
{{- define "audioPreview" }}
{{- if and .PreviewURL .Meta.Small.Width }}
<img
src="{{- .PreviewURL -}}"
loading="lazy"
{{- if .Description }}
alt="{{- .Description -}}"
title="{{- .Description -}}"
{{- end }}
width="{{- .Meta.Small.Width -}}"
height="{{- .Meta.Small.Height -}}"
/>
{{- else }}
<img
src="/assets/logo.webp"
loading="lazy"
{{- if .Description }}
alt="{{- .Description -}}"
title="{{- .Description -}}"
{{- end }}
width="518"
height="460"
/>
{{- end }}
{{- end }}
{{- with . }}
<div class="media-wrapper">
<details class="{{- .Item.Type -}}-spoiler media-spoiler" {{- if not .Item.Sensitive }} open{{- end -}}>
<summary>
<div class="show sensitive button" aria-hidden="true">Show sensitive</div>
<span class="eye button" role="button" tabindex="0" aria-label="Toggle media">
<i class="hide fa fa-fw fa-eye-slash" aria-hidden="true"></i>
<i class="show fa fa-fw fa-eye" aria-hidden="true"></i>
</span>
{{- if or (eq .Item.Type "video") (eq .Item.Type "gifv") }}
{{- include "videoPreview" .Item | indent 3 }}
{{- else if eq .Item.Type "image" }}
{{- include "imagePreview" .Item | indent 3 }}
{{- else if eq .Item.Type "audio" }}
{{- include "audioPreview" .Item | indent 3 }}
{{- end }}
</summary>
{{- if or (eq .Item.Type "video") (eq .Item.Type "gifv") }}
<video
{{- if eq .Item.Type "video" }}
preload="none"
{{- else }}
preload="auto"
muted
{{- end }}
class="plyr-video photoswipe-slide{{- if eq .Item.Type "gifv" }} gifv{{ end }}"
controls
playsinline
data-pswp-index="{{- .Index -}}"
data-pswp-parent-status="{{- .Item.ParentStatusLink -}}"
data-pswp-attachment-id="{{- .Item.ID -}}"
poster="{{- .Item.PreviewURL -}}"
data-pswp-width="{{- .Item.Meta.Original.Width -}}px"
data-pswp-height="{{- .Item.Meta.Original.Height -}}px"
{{- if .Item.Description }}
alt="{{- .Item.Description -}}"
title="{{- .Item.Description -}}"
{{- end }}
>
<source type="{{- .Item.MIMEType -}}" src="{{- .Item.URL -}}"/>
</video>
{{- else if eq .Item.Type "audio" }}
<video
preload="none"
class="plyr-video photoswipe-slide"
controls
playsinline
data-pswp-index="{{- .Index -}}"
data-pswp-parent-status="{{- .Item.ParentStatusLink -}}"
data-pswp-attachment-id="{{- .Item.ID -}}"
{{- if and .Item.PreviewURL .Item.Meta.Small.Width }}
poster="{{- .Item.PreviewURL -}}"
data-pswp-width="{{- .Item.Meta.Small.Width -}}px"
data-pswp-height="{{- .Item.Meta.Small.Height -}}px"
{{- else }}
poster="/assets/logo.webp"
width="518px"
height="460px"
{{- end }}
{{- if .Item.Description }}
alt="{{- .Item.Description -}}"
title="{{- .Item.Description -}}"
{{- end }}
>
<source type="{{- .Item.MIMEType -}}" src="{{- .Item.URL -}}"/>
</video>
{{- else if eq .Item.Type "image" }}
<a
class="photoswipe-slide"
data-pswp-index="{{- .Index -}}"
data-pswp-parent-status="{{- .Item.ParentStatusLink -}}"
data-pswp-attachment-id="{{- .Item.ID -}}"
href="{{- .Item.URL -}}"
target="_blank"
data-pswp-width="{{- .Item.Meta.Original.Width -}}px"
data-pswp-height="{{- .Item.Meta.Original.Height -}}px"
data-cropped="true"
{{- if .Item.Description }}
alt="{{- .Item.Description -}}"
title="{{- .Item.Description -}}"
{{- end }}
>
{{- with .Item }}
{{- include "imagePreview" . | indent 3 }}
{{- end }}
</a>
{{- else }}
<a
class="unknown-attachment"
href="{{- .Item.RemoteURL -}}"
rel="nofollow noreferrer noopener"
target="_blank"
{{- if .Item.Description }}
title="Open external media: {{ .Item.Description -}}&#10;&#13;{{- .Item.RemoteURL -}}"
{{- else }}
title="Open external media.&#10;&#13;{{- .Item.RemoteURL -}}"
{{- end }}
>
<div class="placeholder" aria-hidden="true">
<i class="placeholder-external-link fa fa-external-link"></i>
<i class="placeholder-icon fa fa-file-text"></i>
<div class="placeholder-link-to">External media</div>
</div>
</a>
{{- end }}
</details>
</div>
{{- end }}

View file

@ -1,195 +0,0 @@
{{- /*
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ -}}
{{- /*
Template for rendering a gallery of status media attachments.
To use this template, pass a web view status into it.
*/ -}}
{{- define "imagePreview" }}
<img
src="{{- .PreviewURL -}}"
loading="lazy"
{{- if .Description }}
alt="{{- .Description -}}"
title="{{- .Description -}}"
{{- end }}
width="{{- .Meta.Original.Width -}}"
height="{{- .Meta.Original.Height -}}"
/>
{{- end }}
{{- define "videoPreview" }}
<img
src="{{- .PreviewURL -}}"
loading="lazy"
{{- if .Description }}
alt="{{- .Description -}}"
title="{{- .Description -}}"
{{- end }}
width="{{- .Meta.Small.Width -}}"
height="{{- .Meta.Small.Height -}}"
/>
{{- end }}
{{- define "audioPreview" }}
{{- if and .PreviewURL .Meta.Small.Width }}
<img
src="{{- .PreviewURL -}}"
loading="lazy"
{{- if .Description }}
alt="{{- .Description -}}"
title="{{- .Description -}}"
{{- end }}
width="{{- .Meta.Small.Width -}}"
height="{{- .Meta.Small.Height -}}"
/>
{{- else }}
<img
src="/assets/logo.webp"
loading="lazy"
{{- if .Description }}
alt="{{- .Description -}}"
title="{{- .Description -}}"
{{- end }}
width="518"
height="460"
/>
{{- end }}
{{- end }}
{{- /* Produces something like "1 attachment", "2 attachments", etc */ -}}
{{- define "attachmentsLength" -}}
{{- (len .) }}{{- if eq (len .) 1 }} attachment{{- else }} attachments{{- end -}}
{{- end -}}
{{- /* Produces something like "media photoswipe-gallery odd single" */ -}}
{{- define "galleryClass" -}}
media photoswipe-gallery {{ (len .) | oddOrEven }} {{ if eq (len .) 1 }}single{{ else if eq (len .) 2 }}double{{ end }}
{{- end -}}
{{- with .MediaAttachments }}
<div
class="{{- template "galleryClass" . -}}"
role="group"
aria-label="{{- template "attachmentsLength" . -}}"
>
{{- range $index, $media := . }}
<div class="media-wrapper">
<details class="{{- $media.Type -}}-spoiler media-spoiler" {{- if not $media.Sensitive }} open{{- end -}}>
<summary>
<div class="show sensitive button" aria-hidden="true">Show sensitive media</div>
<span class="eye button" role="button" tabindex="0" aria-label="Toggle media">
<i class="hide fa fa-fw fa-eye-slash" aria-hidden="true"></i>
<i class="show fa fa-fw fa-eye" aria-hidden="true"></i>
</span>
{{- if or (eq .Type "video") (eq .Type "gifv") }}
{{- include "videoPreview" $media | indent 4 }}
{{- else if eq .Type "image" }}
{{- include "imagePreview" $media | indent 4 }}
{{- else if eq .Type "audio" }}
{{- include "audioPreview" $media | indent 4 }}
{{- end }}
</summary>
{{- if or (eq .Type "video") (eq .Type "gifv") }}
<video
{{- if eq .Type "video" }}
preload="none"
{{- else }}
preload="auto"
muted
{{- end }}
class="plyr-video photoswipe-slide{{- if eq .Type "gifv" }} gifv{{ end }}"
controls
playsinline
data-pswp-index="{{- $index -}}"
poster="{{- .PreviewURL -}}"
data-pswp-width="{{- $media.Meta.Small.Width -}}px"
data-pswp-height="{{- $media.Meta.Small.Height -}}px"
{{- if .Description }}
alt="{{- $media.Description -}}"
title="{{- $media.Description -}}"
{{- end }}
>
<source type="{{- $media.MIMEType -}}" src="{{- $media.URL -}}"/>
</video>
{{- else if eq .Type "audio" }}
<video
preload="none"
class="plyr-video photoswipe-slide"
controls
playsinline
data-pswp-index="{{- $index -}}"
{{- if and $media.PreviewURL $media.Meta.Small.Width }}
poster="{{- .PreviewURL -}}"
data-pswp-width="{{- $media.Meta.Small.Width -}}px"
data-pswp-height="{{- $media.Meta.Small.Height -}}px"
{{- else }}
poster="/assets/logo.webp"
width="518px"
height="460px"
{{- end }}
{{- if .Description }}
alt="{{- $media.Description -}}"
title="{{- $media.Description -}}"
{{- end }}
>
<source type="{{- $media.MIMEType -}}" src="{{- $media.URL -}}"/>
</video>
{{- else if eq .Type "image" }}
<a
class="photoswipe-slide"
href="{{- $media.URL -}}"
target="_blank"
data-pswp-width="{{- $media.Meta.Original.Width -}}px"
data-pswp-height="{{- $media.Meta.Original.Height -}}px"
data-cropped="true"
{{- if .Description }}
alt="{{- $media.Description -}}"
title="{{- $media.Description -}}"
{{- end }}
>
{{- with $media }}
{{- include "imagePreview" . | indent 4 }}
{{- end }}
</a>
{{- else }}
<a
class="unknown-attachment"
href="{{- $media.RemoteURL -}}"
rel="nofollow noreferrer noopener"
target="_blank"
{{- if .Description }}
title="Open external media: {{ $media.Description -}}&#10;&#13;{{- $media.RemoteURL -}}"
{{- else }}
title="Open external media.&#10;&#13;{{- $media.RemoteURL -}}"
{{- end }}
>
<div class="placeholder" aria-hidden="true">
<i class="placeholder-external-link fa fa-external-link"></i>
<i class="placeholder-icon fa fa-file-text"></i>
<div class="placeholder-link-to">External media</div>
</div>
</a>
{{- end }}
</details>
</div>
{{- end }}
</div>
{{- end }}

View file

@ -17,36 +17,9 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ -}}
{{- define "visibility_icon" -}}
{{- if eq .Visibility "public" -}}
globe
{{- else if eq .Visibility "unlisted" -}}
unlock
{{- else -}}
question
{{- end -}}
{{- end -}}
{{- define "visibility_title" -}}
{{- if eq .Visibility "public" -}}
Public
{{- else if eq .Visibility "unlisted" -}}
Unlisted
{{- else -}}
Unknown
{{- end -}}
{{- end -}}
{{- with . }}
<dl class="status-stats">
<div class="stats-grouping">
<div class="stats-item visibility-level" title="{{- template "visibility_title" . -}}">
<dt class="sr-only">Visibility</dt>
<dd>
<i class="fa fa-{{- template "visibility_icon" . -}}" aria-hidden="true"></i>
<span class="sr-only">{{- template "visibility_title" . -}}</span>
</dd>
</div>
<div class="stats-item published-at text-cutoff">
<dt class="sr-only">Published</dt>
<dd>
@ -57,7 +30,7 @@
<div class="stats-item edited-at text-cutoff">
<dt class="sr-only">Edited</dt>
<dd>
edited <time class="dt-updated" datetime="{{- .EditedAt -}}">{{- .EditedAt | timestampPrecise -}}</time>
(edited <time class="dt-updated" datetime="{{- .EditedAt -}}">{{- .EditedAt | timestampPrecise -}}</time>)
</dd>
</div>
{{ end }}