Compare commits

..

15 commits

Author SHA1 Message Date
Oneric
4cf4b5e2d0 Merge pull request 'Support selectable visibility of repeats' (#440) from Oneric/akkoma-fe:boost-scopes into develop
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma-fe/pulls/440
2025-06-10 18:37:59 +00:00
jadiunr
d617a9596a Add support selectable visibility of repeat
Co-authored-by: Oneric <oneric@oneric.stub>
2025-05-18 22:52:55 +02:00
Oneric
4734e9668d refactor: extract scope logic into shared module 2025-05-18 22:52:55 +02:00
Oneric
9787f43343 Merge pull request 'Check for canvas extract permission when initializing favicon service' (#432) from mkljczk/akkoma-fe:check-canvas-extract-permission into develop
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma-fe/pulls/432
2025-05-09 18:34:53 +00:00
Oneric
61bdedc82f Merge pull request 'remove some jank in emoji reacts component' (#435) from Riedler/akkoma-fe:fix-reacts into develop
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma-fe/pulls/435
2025-05-09 18:30:07 +00:00
Oneric
a4eddc7f1c Merge pull request 'polls: base fractions on voters for multiple choice polls' (#436) from Oneric/akkoma-fe:poll-percentages into develop
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma-fe/pulls/436
2025-05-09 18:29:54 +00:00
sn0w
94c5998593 Apply wordfilters to attachment alt-texts
EDITED to apply review suggestions:
  - short circuit search and immediately return once match found
  - Array.some() instead of for loop
2025-05-05 22:39:43 +02:00
Oneric
851dd263c0 docs/sticker: fix example setup 2025-04-25 00:45:04 +02:00
Oneric
473ba89355 polls: base fractions on voters for multiple choice polls
This allows discerning how many voters agreed
with an option and aligns with other clients.
However, a backend bug makes this impossible for
remote multiple choice polls, so retain current
behaviour for anything affected.
2025-04-04 19:27:30 +02:00
Riedler
4ce8ffcec1 fix: shrink unicode emojis in reactions slightly
some large ones exceeded container boundaries before
2025-03-26 09:09:56 +01:00
Riedler
e62b154228 fix: uniform height sizing and layouting 2025-03-26 07:39:54 +01:00
Riedler
e87a9ced61 fix: no more emojis bleeding into button borders 2025-03-26 07:12:45 +01:00
Riedler
7245775b27 fix: picked reactions should be positioned identically 2025-03-26 06:56:32 +01:00
mkljczk
6373c5a05d Check for canvas extract permission when initializing favicon service 2025-03-05 15:02:16 +00:00
Floatingghost
2914eaf1ca Revert "reduce gallery size"
This reverts commit 06ba190e2e.
2025-03-01 16:14:55 +00:00
14 changed files with 141 additions and 61 deletions

View file

@ -15,12 +15,13 @@ put a file that looks like this
```json ```json
{ {
"myPack": "/static/stickers/myPack" "myPack": "/static/stickers/myPack/"
} }
``` ```
This file is a mapping from name to pack directory location. It says "we have a pack called myPack, look for This file is a mapping from name to pack directory location. It says "we have a pack called myPack, look for
it at `/static/stickers/myPack`". You can add as many packs as you like in this manner. it inside `/static/stickers/myPack`". You can add as many packs as you like in this manner.
Note that a single leading and a trailing slash are **required** to work correctly!
## Creating the pack ## Creating the pack

View file

@ -1,6 +1,6 @@
{ {
"name": "pleroma_fe", "name": "pleroma_fe",
"version": "3.11.0", "version": "3.10.0",
"description": "A frontend for Akkoma instances", "description": "A frontend for Akkoma instances",
"author": "Roger Braun <roger@rogerbraun.net>", "author": "Roger Braun <roger@rogerbraun.net>",
"private": true, "private": true,

View file

@ -11,7 +11,7 @@
@click="emojiOnClick(reaction.name, $event)" @click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()" @mouseenter="fetchEmojiReactionsByIfMissing()"
> >
<span <template
v-if="reaction.url !== null" v-if="reaction.url !== null"
> >
<StillImage <StillImage
@ -19,16 +19,15 @@
:title="reaction.name" :title="reaction.name"
:alt="reaction.name" :alt="reaction.name"
class="reaction-emoji" class="reaction-emoji"
height="2.55em"
/> />
{{ reaction.count }} {{ reaction.count }}
</span> </template>
<span v-else> <template v-else>
<span class="reaction-emoji unicode-emoji"> <span class="reaction-emoji unicode-emoji">
{{ reaction.name }} {{ reaction.name }}
</span> </span>
<span>{{ reaction.count }}</span> <span>{{ reaction.count }}</span>
</span> </template>
</button> </button>
</UserListPopover> </UserListPopover>
<a <a
@ -53,23 +52,26 @@
container-type: inline-size; container-type: inline-size;
} }
.unicode-emoji {
font-size: 210%;
}
.emoji-reaction { .emoji-reaction {
padding: 0 0.5em; padding: 2px 0.5em;
margin-right: 0.5em; margin-right: 0.5em;
margin-top: 0.5em; margin-top: 0.5em;
display: flex; display: flex;
align-items: center; align-items: end;
justify-content: center;
box-sizing: border-box;
.reaction-emoji { .reaction-emoji {
width: auto; width: auto;
max-width: 96cqw; max-width: 96cqw;
height: 2.55em !important;
margin-right: 0.25em; margin-right: 0.25em;
&.still-image {
height: 2.55em;
}
&.unicode-emoji {
display: inline-block;
font-size: 2.125em; // assuming default line height of 1.2rem and emojis that don't exceed line height
line-height: 2.55rem;
}
} }
&:focus { &:focus {
outline: none; outline: none;
@ -97,9 +99,9 @@
} }
.button-default.picked-reaction { .button-default.picked-reaction {
border: 1px solid var(--accent, $fallback--link); &, &:hover {
margin-left: -1px; // offset the border, can't use inset shadows either box-shadow: inset 0 0 0 1px var(--accent, $fallback--link);
margin-right: calc(0.5em - 1px); }
} }
</style> </style>

View file

@ -99,7 +99,7 @@
width: 100%; width: 100%;
flex-grow: 1; flex-grow: 1;
.Status & { .Status & {
max-height: 10em; max-height: 30em;
} }
&.-audio { &.-audio {

View file

@ -50,6 +50,13 @@ export default {
totalVotesCount () { totalVotesCount () {
return this.poll.votes_count return this.poll.votes_count
}, },
totalFractionBase () {
// Due to a backend bug, we might not have any voter count info for remote polls
// in this case, fall back to count of votes even for multiple cjoice polls
// to be able to at least display _something_
const total_base = this.poll.multiple ? this.poll.voters_count : this.poll.votes_count
return total_base > 0 ? total_base : this.poll.votes_count
},
containerClass () { containerClass () {
return { return {
loading: this.loading loading: this.loading
@ -70,10 +77,11 @@ export default {
}, },
methods: { methods: {
percentageForOption (count) { percentageForOption (count) {
return this.totalVotesCount === 0 ? 0 : Math.round(count / this.totalVotesCount * 100) const total = this.totalFractionBase
return total === 0 ? 0 : Math.round(count / total * 100)
}, },
resultTitle (option) { resultTitle (option) {
return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}` return `${option.votes_count}/${this.totalFractionBase} ${this.$t('polls.votes')}`
}, },
fetchPoll () { fetchPoll () {
this.$store.dispatch('refreshPoll', { id: this.statusId, pollId: this.poll.id }) this.$store.dispatch('refreshPoll', { id: this.statusId, pollId: this.poll.id })

View file

@ -10,6 +10,7 @@ import fileTypeService from '../../services/file_type/file_type.service.js'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js' import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
import { reject, map, uniqBy, debounce } from 'lodash' import { reject, map, uniqBy, debounce } from 'lodash'
import { usePostLanguageOptions } from 'src/lib/post_language' import { usePostLanguageOptions } from 'src/lib/post_language'
import scopeUtils from 'src/lib/scope_utils.js'
import suggestor from '../emoji_input/suggestor.js' import suggestor from '../emoji_input/suggestor.js'
import { mapGetters, mapState } from 'vuex' import { mapGetters, mapState } from 'vuex'
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
@ -762,15 +763,9 @@ const PostStatusForm = {
this.$store.dispatch('openSettingsModalTab', 'profile') this.$store.dispatch('openSettingsModalTab', 'profile')
}, },
suggestedVisibility () { suggestedVisibility () {
if (this.copyMessageScope) { const maxScope = this.copyMessageScope
if (this.copyMessageScope === 'direct') { const defaultScope = this.$store.state.users.currentUser.default_scope
return this.copyMessageScope return scopeUtils.negotiate(defaultScope, maxScope)
}
if (this.copyMessageScope !== 'public' && this.$store.state.users.currentUser.default_scope !== 'private') {
return this.copyMessageScope
}
}
return this.$store.state.users.currentUser.default_scope
} }
} }
} }

View file

@ -1,4 +1,6 @@
import ConfirmModal from '../confirm_modal/confirm_modal.vue' import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import ScopeSelector from '../scope_selector/scope_selector.vue'
import scopeUtils from 'src/lib/scope_utils.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faRetweet } from '@fortawesome/free-solid-svg-icons' import { faRetweet } from '@fortawesome/free-solid-svg-icons'
@ -7,12 +9,14 @@ library.add(faRetweet)
const RetweetButton = { const RetweetButton = {
props: ['status', 'loggedIn', 'visibility'], props: ['status', 'loggedIn', 'visibility'],
components: { components: {
ConfirmModal ConfirmModal,
ScopeSelector
}, },
data () { data () {
return { return {
animated: false, animated: false,
showingConfirmDialog: false showingConfirmDialog: false,
retweetVisibility: this.$store.state.users.currentUser.default_scope
} }
}, },
methods: { methods: {
@ -25,7 +29,7 @@ const RetweetButton = {
}, },
doRetweet () { doRetweet () {
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: this.retweetVisibility })
} else { } else {
this.$store.dispatch('unretweet', { id: this.status.id }) this.$store.dispatch('unretweet', { id: this.status.id })
} }
@ -40,6 +44,9 @@ const RetweetButton = {
}, },
hideConfirmDialog () { hideConfirmDialog () {
this.showingConfirmDialog = false this.showingConfirmDialog = false
},
changeVis (visibility) {
this.retweetVisibility = visibility
} }
}, },
computed: { computed: {
@ -54,6 +61,15 @@ const RetweetButton = {
}, },
remoteInteractionLink () { remoteInteractionLink () {
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id }) return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
},
userDefaultScope () {
return this.$store.state.users.currentUser.default_scope
},
statusScope () {
return this.status.visibility
},
initialScope () {
return scopeUtils.negotiate(this.userDefaultScope, this.status.visibility)
} }
} }
} }

View file

@ -49,6 +49,12 @@
@cancelled="hideConfirmDialog" @cancelled="hideConfirmDialog"
> >
{{ $t('status.repeat_confirm') }} {{ $t('status.repeat_confirm') }}
<scope-selector
:user-default="userDefaultScope"
:original-scope="statusScope"
:initial-scope="initialScope"
:on-scope-change="changeVis"
/>
</confirm-modal> </confirm-modal>
</teleport> </teleport>
</div> </div>

View file

@ -6,6 +6,8 @@ import {
faGlobe faGlobe
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import scopeUtils from 'src/lib/scope_utils.js'
library.add( library.add(
faEnvelope, faEnvelope,
faGlobe, faGlobe,
@ -13,18 +15,11 @@ library.add(
faLockOpen faLockOpen
) )
const SCOPE_LEVELS = {
'direct': 0,
'private': 1,
'local': 2,
'unlisted': 2,
'public': 3
}
const ScopeSelector = { const ScopeSelector = {
props: [ props: [
'showAll', 'showAll',
'userDefault', 'userDefault',
// scope of parent object
'originalScope', 'originalScope',
'initialScope', 'initialScope',
'onScopeChange' 'onScopeChange'
@ -39,16 +34,16 @@ const ScopeSelector = {
return !this.showPublic && !this.showUnlisted && !this.showPrivate && !this.showDirect return !this.showPublic && !this.showUnlisted && !this.showPrivate && !this.showDirect
}, },
showPublic () { showPublic () {
return this.originalScope !== 'direct' && this.shouldShow('public') return this.shouldShow('public')
}, },
showLocal () { showLocal () {
return this.originalScope !== 'direct' && this.shouldShow('local') return this.shouldShow('local')
}, },
showUnlisted () { showUnlisted () {
return this.originalScope !== 'direct' && this.shouldShow('unlisted') return this.shouldShow('unlisted')
}, },
showPrivate () { showPrivate () {
return this.originalScope !== 'direct' && this.shouldShow('private') return this.shouldShow('private')
}, },
showDirect () { showDirect () {
return this.shouldShow('direct') return this.shouldShow('direct')
@ -65,15 +60,10 @@ const ScopeSelector = {
}, },
methods: { methods: {
shouldShow (scope) { shouldShow (scope) {
if (!this.originalScope) { if (!this.originalScope)
return true return true
} else
return scopeUtils.compare(scope, this.originalScope) <= 0
if (this.originalScope === 'local') {
return scope === 'direct' || scope === 'local'
}
return SCOPE_LEVELS[scope] <= SCOPE_LEVELS[this.originalScope]
}, },
changeVis (scope) { changeVis (scope) {
this.currentScope = scope this.currentScope = scope

37
src/lib/scope_utils.js Normal file
View file

@ -0,0 +1,37 @@
const SCOPE_LEVELS = {
'direct': 0,
'private': 1,
'unlisted': 2,
'local': 3,
'public': 3
}
export default {
negotiate: (defaultScope, maxScope) => {
if (!maxScope)
return defaultScope;
if (maxScope === 'local')
return defaultScope === 'direct' ? defaultScope : 'local';
if (SCOPE_LEVELS[defaultScope] <= SCOPE_LEVELS[maxScope])
return defaultScope;
else
return maxScope;
},
compare: (sa, sb) => {
if (sa === 'local') {
if (sb === 'direct')
return 1;
else if (sb === sa)
return 0;
else
return -1;
}
if (sa === sb)
return 0;
return SCOPE_LEVELS[sa] < SCOPE_LEVELS[sb] ? -1 : 1;
}
}

View file

@ -663,10 +663,10 @@ const statuses = {
return rootState.api.backendInteractor.unmuteConversation({ id: statusId }) return rootState.api.backendInteractor.unmuteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status)) .then((status) => commit('setMutedStatus', status))
}, },
retweet ({ rootState, commit }, status) { retweet ({ rootState, commit }, {id, visibility}) {
// Optimistic retweeting... // Optimistic retweeting...
commit('setRetweeted', { status, value: true }) commit('setRetweeted', { status: {id: id}, value: true })
rootState.api.backendInteractor.retweet({ id: status.id }) rootState.api.backendInteractor.retweet({ id: id, visibility: 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

@ -822,8 +822,8 @@ const unfavorite = ({ id, credentials }) => {
.then((data) => parseStatus(data)) .then((data) => parseStatus(data))
} }
const retweet = ({ id, credentials }) => { const retweet = ({ id, visibility, credentials }) => {
return promisedRequest({ url: MASTODON_RETWEET_URL(id), method: 'POST', credentials }) return promisedRequest({ url: MASTODON_RETWEET_URL(id), method: 'POST', payload: { visibility }, credentials })
.then((data) => parseStatus(data)) .then((data) => parseStatus(data))
} }

View file

@ -1,3 +1,19 @@
const checkCanvasExtractPermission = () => {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext('2d');
if (!ctx) return false;
ctx.fillStyle = '#0f161e';
ctx.fillRect(0, 0, 1, 1);
const { data } = ctx.getImageData(0, 0, 1, 1);
return data.join(',') === '15,22,30,255';
};
const createFaviconService = () => { const createFaviconService = () => {
const favicons = [] const favicons = []
const faviconWidth = 128 const faviconWidth = 128
@ -6,6 +22,8 @@ const createFaviconService = () => {
const gapWidth = 24 const gapWidth = 24
const initFaviconService = () => { const initFaviconService = () => {
if (!checkCanvasExtractPermission()) return;
const nodes = document.querySelectorAll('link[rel="icon"]') const nodes = document.querySelectorAll('link[rel="icon"]')
nodes.forEach(favicon => { nodes.forEach(favicon => {
if (favicon) { if (favicon) {

View file

@ -3,8 +3,15 @@ import { filter } from 'lodash'
export const muteWordHits = (status, muteWords) => { export const muteWordHits = (status, muteWords) => {
const statusText = status.text.toLowerCase() const statusText = status.text.toLowerCase()
const statusSummary = status.summary.toLowerCase() const statusSummary = status.summary.toLowerCase()
const hits = filter(muteWords, (muteWord) => { const hits = filter(muteWords, (muteWord) => {
return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase()) muteWord = muteWord.toLowerCase()
return (
statusText.includes(muteWord) ||
statusSummary.includes(muteWord) ||
status.attachments.some((a) => a.description?.toLowerCase().includes(muteWord))
)
}) })
return hits return hits