Compare commits

...

5 commits

Author SHA1 Message Date
tea
ed96bd4672 enable pull-to-refresh 2025-01-22 17:00:53 +01:00
tea
25eb0b7e68 add support for repeat with visibility
This requires backend changes to work properly.

Currently Akkoma maps:
* public and unlisted (and local?) to public
* private to private
2025-01-22 17:00:53 +01:00
tea
d4a5f882b3 display repeat visibility 2025-01-22 17:00:53 +01:00
tea
a554dcb8d6 fix popovers in status content being cut off (this might break other things but we'll see)
this affected popovers like hovering over mentions without instance name
2025-01-22 17:00:53 +01:00
tea
e5da629c41 align profile header top center 2025-01-22 17:00:53 +01:00
9 changed files with 169 additions and 68 deletions

View file

@ -20,7 +20,7 @@ body {
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
overscroll-behavior-y: none; overscroll-behavior-y: auto;
overflow-x: clip; overflow-x: clip;
overflow-y: scroll; overflow-y: scroll;

View file

@ -1,12 +1,14 @@
import Popover from '../popover/popover.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue' import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faRetweet } from '@fortawesome/free-solid-svg-icons' import { faRetweet, faTrash } from '@fortawesome/free-solid-svg-icons'
library.add(faRetweet) library.add(faRetweet, faTrash)
const RetweetButton = { const RetweetButton = {
props: ['status', 'loggedIn', 'visibility'], props: ['status', 'loggedIn', 'visibility'],
components: { components: {
Popover,
ConfirmModal ConfirmModal
}, },
data () { data () {
@ -16,16 +18,16 @@ const RetweetButton = {
} }
}, },
methods: { methods: {
retweet () { retweet (visibility) {
if (!this.status.repeated && this.shouldConfirmRepeat) { if (!this.status.repeated && this.shouldConfirmRepeat) {
this.showConfirmDialog() this.showConfirmDialog()
} else { } else {
this.doRetweet() this.doRetweet(visibility)
} }
}, },
doRetweet () { doRetweet (visibility) {
if (!this.status.repeated) { if (!this.status.repeated) {
this.$store.dispatch('retweet', { id: this.status.id }) this.$store.dispatch('retweet', { id: this.status.id, visibility: visibility })
} else { } else {
this.$store.dispatch('unretweet', { id: this.status.id }) this.$store.dispatch('unretweet', { id: this.status.id })
} }

View file

@ -1,57 +1,124 @@
<template> <template>
<div class="RetweetButton"> <!-- TODO settings the offset like this feels like a hack -->
<button <Popover
v-if="(visibility !== 'private' || isOwn) && visibility !== 'direct' && loggedIn" class="RetweetButton"
class="button-unstyled interactive" trigger="click"
:class="status.repeated && '-repeated'" placement="bottom"
:title="$t('tool_tip.repeat')" :offset="{ y: 22 }"
@click.prevent="retweet()" :bound-to="{ x: 'container' }"
> remove-padding
<FAIcon >
class="fa-scale-110 fa-old-padding" <template v-slot:content="{close}">
icon="retweet" <div class="dropdown-menu">
:spin="animated" <template v-if="status.repeated">
/> <button
</button> v-if="status.repeated"
<span v-else-if="loggedIn"> class="button-default dropdown-item dropdown-item-icon"
<FAIcon @click.prevent="retweet()"
class="fa-scale-110 fa-old-padding" >
icon="lock" <!-- TODO: i18n -->
:title="$t('timeline.no_retweet_hint')" <FAIcon
/> fixed-width
</span> icon="trash"
<a /><span>Undo repeat</span>
v-else </button>
class="button-unstyled interactive" </template>
target="_blank" <template v-else>
role="button" <!-- TODO: don't show button that would increase post visibility -->
:href="remoteInteractionLink" <button
> class="button-default dropdown-item dropdown-item-icon"
<FAIcon @click.prevent="retweet('public')"
class="fa-scale-110 fa-old-padding" >
icon="retweet" <FAIcon
fixed-width
icon="globe"
/><span>{{ $t("general.scope_in_timeline.public") }}</span>
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="retweet('unlisted')"
>
<FAIcon
fixed-width
icon="lock-open"
/><span>{{ $t("general.scope_in_timeline.unlisted") }}</span>
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="retweet('private')"
>
<FAIcon
fixed-width
icon="lock"
/><span>{{ $t("general.scope_in_timeline.private") }}</span>
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="retweet('local')"
>
<!-- TODO: "general.scope_in_timeline.local" feels too long for the popover -->
<FAIcon
fixed-width
icon="users"
/><span>Local</span>
</button>
</template>
</div>
</template>
<template v-slot:trigger>
<button
v-if="(visibility !== 'private' || isOwn) && visibility !== 'direct' && loggedIn"
class="button-unstyled popover-trigger"
:class="status.repeated && '-repeated'"
:title="$t('tool_tip.repeat')" :title="$t('tool_tip.repeat')"
/>
</a>
<span
v-if="!mergedConfig.hidePostStats && status.repeat_num > 0"
class="no-event"
>
{{ status.repeat_num }}
</span>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmDialog"
:title="$t('status.repeat_confirm_title')"
:confirm-text="$t('status.repeat_confirm_accept_button')"
:cancel-text="$t('status.repeat_confirm_cancel_button')"
@accepted="doRetweet"
@cancelled="hideConfirmDialog"
> >
{{ $t('status.repeat_confirm') }} <FAIcon
</confirm-modal> class="fa-scale-110 fa-old-padding"
</teleport> icon="retweet"
</div> :spin="animated"
/>
</button>
<span v-else-if="loggedIn">
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="lock"
:title="$t('timeline.no_retweet_hint')"
/>
</span>
<a
v-else
class="button-unstyled interactive"
target="_blank"
role="button"
:href="remoteInteractionLink"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="retweet"
:title="$t('tool_tip.repeat')"
/>
</a>
<span
v-if="!mergedConfig.hidePostStats && status.repeat_num > 0"
class="no-event"
>
{{ status.repeat_num }}
</span>
<teleport to="#modal">
<!-- TODO does this work for all? -->
<confirm-modal
v-if="showingConfirmDialog"
:title="$t('status.repeat_confirm_title')"
:confirm-text="$t('status.repeat_confirm_accept_button')"
:cancel-text="$t('status.repeat_confirm_cancel_button')"
@accepted="doRetweet"
@cancelled="hideConfirmDialog"
>
{{ $t('status.repeat_confirm') }}
</confirm-modal>
</teleport>
</template>
</Popover>
</template> </template>
<script src="./retweet_button.js"></script> <script src="./retweet_button.js"></script>
@ -72,7 +139,7 @@
user-select: none; user-select: none;
} }
.interactive { .popover-trigger {
.svg-inline--fa { .svg-inline--fa {
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
animation: unset; animation: unset;
@ -80,6 +147,7 @@
animation-duration: 0.6s; animation-duration: 0.6s;
} }
/* TODO: don't turn green on hover because it keeps being focused on mobile when the popover is open. Instead make it while(?) like in extra_buttons and only green once it's been repeated. */
&:hover .svg-inline--fa, &:hover .svg-inline--fa,
&.-repeated .svg-inline--fa { &.-repeated .svg-inline--fa {
color: $fallback--cGreen; color: $fallback--cGreen;

View file

@ -215,6 +215,7 @@ const Status = {
retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name_ui }, retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name_ui },
retweeterHtml () { return this.statusoid.user.name }, retweeterHtml () { return this.statusoid.user.name },
retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) }, retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) },
retweeterVisibility () { return this.statusoid.visibility },
status () { status () {
if (this.retweet) { if (this.retweet) {
return this.statusoid.retweeted_status return this.statusoid.retweeted_status
@ -440,6 +441,9 @@ const Status = {
visibilityLocalized () { visibilityLocalized () {
return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility) return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility)
}, },
retweeterVisibilityLocalized () {
return this.$i18n.t('general.scope_in_timeline.' + this.statusoid.visibility)
},
isEdited () { isEdited () {
return this.status.edited_at !== null return this.status.edited_at !== null
}, },

View file

@ -42,10 +42,6 @@
display: flex; display: flex;
padding: var(--status-margin, $status-margin); padding: var(--status-margin, $status-margin);
.content {
overflow: hidden;
}
> * { > * {
min-width: 0; min-width: 0;
} }

View file

@ -113,6 +113,18 @@
/> />
{{ $t('timeline.repeated') }} {{ $t('timeline.repeated') }}
</div> </div>
<span
v-if="retweeterVisibility"
class="visibility-icon"
:title="retweeterVisibilityLocalized"
>
<FAIcon
fixed-width
class="fa-scale-110"
:icon="visibilityIcon(retweeterVisibility)"
/>
</span>
</div> </div>
</div> </div>

View file

@ -48,6 +48,7 @@
-webkit-mask-composite: xor; -webkit-mask-composite: xor;
mask-composite: exclude; mask-composite: exclude;
background-size: cover; background-size: cover;
background-position: top;
mask-size: 100% 60%; mask-size: 100% 60%;
border-top-left-radius: calc(var(--panelRadius) - 1px); border-top-left-radius: calc(var(--panelRadius) - 1px);
border-top-right-radius: calc(var(--panelRadius) - 1px); border-top-right-radius: calc(var(--panelRadius) - 1px);

View file

@ -666,7 +666,7 @@ const statuses = {
retweet ({ rootState, commit }, status) { retweet ({ rootState, commit }, status) {
// Optimistic retweeting... // Optimistic retweeting...
commit('setRetweeted', { status, value: true }) commit('setRetweeted', { status, value: true })
rootState.api.backendInteractor.retweet({ id: status.id }) rootState.api.backendInteractor.retweet({ id: status.id, visibility: status.visibility })
.then(status => commit('setRetweetedConfirm', { status: status.retweeted_status, user: rootState.users.currentUser })) .then(status => commit('setRetweetedConfirm', { status: status.retweeted_status, user: rootState.users.currentUser }))
}, },
unretweet ({ rootState, commit }, status) { unretweet ({ rootState, commit }, status) {

View file

@ -125,21 +125,31 @@ let fetch = (url, options) => {
} }
const promisedRequest = ({ method, url, params, payload, credentials, headers = {} }) => { const promisedRequest = ({ method, url, params, payload, credentials, headers = {} }) => {
const payloadIsFormData = payload instanceof FormData
const options = { const options = {
method, method,
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json',
...headers ...headers
} }
} }
if (!payloadIsFormData) {
// only set content type if payload is not form data
// if it is unset the content type will be automatically set to multipart/form-data
options.headers['Content-Type'] = 'application/json'
}
if (params) { if (params) {
url += '?' + Object.entries(params) url += '?' + Object.entries(params)
.map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value)) .map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value))
.join('&') .join('&')
} }
if (payload) { if (payload) {
options.body = JSON.stringify(payload) if (payloadIsFormData) {
options.body = payload
} else {
options.body = JSON.stringify(payload)
}
} }
if (credentials) { if (credentials) {
options.headers = { options.headers = {
@ -822,8 +832,16 @@ const unfavorite = ({ id, credentials }) => {
.then((data) => parseStatus(data)) .then((data) => parseStatus(data))
} }
const retweet = ({ id, credentials }) => { const retweet = ({ id, credentials, visibility }) => {
return promisedRequest({ url: MASTODON_RETWEET_URL(id), method: 'POST', credentials }) const form = new FormData()
if (visibility) form.append('visibility', visibility)
return promisedRequest({
url: MASTODON_RETWEET_URL(id),
payload: form,
method: 'POST',
credentials
})
.then((data) => parseStatus(data)) .then((data) => parseStatus(data))
} }