mirror of
https://akkoma.dev/AkkomaGang/akkoma-fe
synced 2025-07-02 01:37:19 +08:00
Compare commits
15 commits
Author | SHA1 | Date | |
---|---|---|---|
|
4cf4b5e2d0 | ||
|
d617a9596a | ||
|
4734e9668d | ||
|
9787f43343 | ||
|
61bdedc82f | ||
|
a4eddc7f1c | ||
|
94c5998593 | ||
|
851dd263c0 | ||
|
473ba89355 | ||
|
4ce8ffcec1 | ||
|
e62b154228 | ||
|
e87a9ced61 | ||
|
7245775b27 | ||
|
6373c5a05d | ||
|
2914eaf1ca |
14 changed files with 141 additions and 61 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -99,7 +99,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
.Status & {
|
.Status & {
|
||||||
max-height: 10em;
|
max-height: 30em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-audio {
|
&.-audio {
|
||||||
|
|
|
@ -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 })
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
37
src/lib/scope_utils.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue