forked from AkkomaGang/akkoma-fe
Compare commits
26 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c57e59f8e8 | ||
| 061a9ad325 | |||
|
|
4d578720e8 | ||
|
|
c98962f4b3 | ||
|
|
2292381b0a | ||
|
|
0f695386fe | ||
|
|
7fb67ee723 | ||
|
|
e80ebc3fac | ||
| e6c0d35d29 | |||
|
8f5cf700f8 |
|||
|
|
efe15c98c6 | ||
|
|
a734eda0d9 | ||
|
51caf0430f |
|||
| 48905a4431 | |||
| c465cb0a35 | |||
|
|
affbc240d1 | ||
| a123b41a2f | |||
| 4ab3424508 | |||
| b04e4810f8 | |||
| fc8debd2c4 | |||
|
|
8227c84aa2 | ||
|
|
42595fcb2c | ||
|
|
e3a72827ef | ||
| 34e4928754 | |||
|
|
9bfd3936d6 | ||
|
|
8d8e6d979a |
51 changed files with 485 additions and 172 deletions
|
|
@ -1,5 +1,5 @@
|
|||
labels:
|
||||
platform: linux/amd64
|
||||
platform: linux/arm64
|
||||
|
||||
steps:
|
||||
lint:
|
||||
|
|
@ -51,8 +51,8 @@ steps:
|
|||
from_secret: SCW_DEFAULT_ORGANIZATION_ID
|
||||
commands:
|
||||
- apt-get update && apt-get install -y rclone wget zip
|
||||
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64
|
||||
- mv scaleway-cli_2.30.0_linux_amd64 scaleway-cli
|
||||
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_arm64
|
||||
- mv scaleway-cli_2.30.0_linux_arm64 scaleway-cli
|
||||
- chmod +x scaleway-cli
|
||||
- ./scaleway-cli object config install type=rclone
|
||||
- zip akkoma-fe.zip -r dist
|
||||
|
|
@ -76,8 +76,8 @@ steps:
|
|||
image: python:3.10-slim
|
||||
commands:
|
||||
- apt-get update && apt-get install -y rclone wget git zip
|
||||
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64
|
||||
- mv scaleway-cli_2.30.0_linux_amd64 scaleway-cli
|
||||
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_arm64
|
||||
- mv scaleway-cli_2.30.0_linux_arm64 scaleway-cli
|
||||
- chmod +x scaleway-cli
|
||||
- ./scaleway-cli object config install type=rclone
|
||||
- cd docs
|
||||
|
|
|
|||
35
CHANGELOG.md
35
CHANGELOG.md
|
|
@ -3,12 +3,41 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## Unreleased
|
||||
## Unreleased (3.19)
|
||||
### Added
|
||||
- lists UI can now read and set the "exclusive" parameter, allowing members to be removed from the home timeline
|
||||
- user profiles now have a small gallery for profile media
|
||||
- alt text of user profile media is now exposed and can be edited in profile settings
|
||||
- if known, polls now show what promise wrt to vote anonymity was made
|
||||
|
||||
### Fixed
|
||||
- fix error on list creation preventing initial accounts from being actually added
|
||||
- fix notifications on mobile
|
||||
- fix attachment display for remotes not federating any MIME type indicators if they still indicate a generic type.
|
||||
This applies to e.g. bridgy
|
||||
|
||||
## 2026.03 (3.18.0) - 2026-03-14
|
||||
### REMOVED
|
||||
- dropped obsolete and buggy dm timeline
|
||||
|
||||
### Added
|
||||
- UI for conversations API, replacing the DM timeline.
|
||||
Here each thread (conversation) has it’s own timeline and read markers instead of mixing everything together.
|
||||
- Boosts now show when and with which visibility they were boosted
|
||||
- bookmarks are now accessible via the narrow/mobile UI
|
||||
|
||||
### Fixed
|
||||
- fixed saving fallback cop yof settings to local browser storage
|
||||
- improve image animation detection further
|
||||
- fix status content parsing for mention and hashtag detection; this could lock the UI until reload
|
||||
- fix display of nsfw attachment overlays on webkit
|
||||
|
||||
## Between 2022.09 (3.2.0) and 2025.12 (3.17.0)
|
||||
A whole lot of stuff, but we forgot to update the changelog besides the one entry below, oopsi
|
||||
|
||||
- Implemented remote interaction with statuses
|
||||
|
||||
|
||||
## 2022.09 - 2022-09-10
|
||||
## 2022.09 (3.2.0) - 2022-09-10
|
||||
### Added
|
||||
- Automatic post translations. Must be configured on the backend in order to work.
|
||||
- Post editing, including a log of previous edits.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "pleroma_fe",
|
||||
"version": "3.12.0",
|
||||
"version": "3.18.0",
|
||||
"description": "A frontend for Akkoma instances",
|
||||
"author": "Roger Braun <roger@rogerbraun.net>",
|
||||
"private": true,
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const Attachment = {
|
|||
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
|
||||
preloadImage: this.$store.getters.mergedConfig.preloadImage,
|
||||
loading: false,
|
||||
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
|
||||
img: fileTypeService.fileType(this.attachment) === 'image' && document.createElement('img'),
|
||||
modalOpen: false,
|
||||
showHidden: false,
|
||||
flashLoaded: false,
|
||||
|
|
@ -105,7 +105,7 @@ const Attachment = {
|
|||
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
||||
},
|
||||
type () {
|
||||
return fileTypeService.fileType(this.attachment.mimetype)
|
||||
return fileTypeService.fileType(this.attachment)
|
||||
},
|
||||
hidden () {
|
||||
return this.nsfw && this.hideNsfwLocal && !this.showHidden
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.-nsfw-placeholder {
|
||||
.attachment-wrapper {
|
||||
align-content: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.description-container {
|
||||
flex: 0 1 0;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@
|
|||
<div class="heading">
|
||||
<div class="title-bar">
|
||||
<div class="title-bar-left">
|
||||
<div class="unread"
|
||||
<div
|
||||
v-if="conversation.unread"
|
||||
class="unread"
|
||||
>
|
||||
<span
|
||||
class="badge badge-notification"
|
||||
|
|
@ -29,7 +30,7 @@
|
|||
</button>
|
||||
|
||||
</div>
|
||||
<h4>{{ $t('dm_conv.default_name', {id: this.conversation.id}) }}</h4>
|
||||
<h4>{{ $t('dm_conv.default_name', {id: conversation.id}) }}</h4>
|
||||
</div>
|
||||
<div class="title-bar-right">
|
||||
<button
|
||||
|
|
@ -48,17 +49,21 @@
|
|||
<div class="members">
|
||||
<UserAvatar
|
||||
v-for="user in membersTruncated.users"
|
||||
:key="user.id"
|
||||
:user="user"
|
||||
:compact="compact"
|
||||
/>
|
||||
<div v-if="membersTruncated.truncated" class="ellipsis">
|
||||
<div
|
||||
v-if="membersTruncated.truncated"
|
||||
class="ellipsis"
|
||||
>
|
||||
...
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<div
|
||||
class="last-message"
|
||||
v-if="showLastStatus"
|
||||
class="last-message"
|
||||
>
|
||||
<div class="last-message-title">
|
||||
{{ $t('dm_conv.last_message_title') }}:
|
||||
|
|
@ -76,7 +81,7 @@
|
|||
<button
|
||||
class="btn button-default"
|
||||
:title="$t('dm_conv.recipients_edit_mode_button_tooltip')"
|
||||
@click.once="$router.push({ name: 'dm-conversation-recipients', params: { id: this.conversation.id }})"
|
||||
@click.once="$router.push({ name: 'dm-conversation-recipients', params: { id: conversation.id }})"
|
||||
>
|
||||
{{ $t('dm_conv.recipients_edit_mode_button') }}
|
||||
</button>
|
||||
|
|
@ -90,7 +95,7 @@
|
|||
@accepted="doDeleteConversation"
|
||||
@cancelled="hideDeleteConfirmModal"
|
||||
>
|
||||
{{ $t('dm_conv.delete_confirm', { identifier: this.conversation.id }) }}
|
||||
{{ $t('dm_conv.delete_confirm', { identifier: conversation.id }) }}
|
||||
</confirm-modal>
|
||||
</teleport>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,15 +6,15 @@
|
|||
timeline-name="dmConv"
|
||||
>
|
||||
<template
|
||||
v-slot:extraHeading
|
||||
#extraHeading
|
||||
>
|
||||
<DMConvCard
|
||||
v-if="conversation"
|
||||
:conversation="conversation"
|
||||
:compact="false"
|
||||
:showFullControls="true"
|
||||
:showLastStatus="false"
|
||||
:linkToTimeline="false"
|
||||
:show-full-controls="true"
|
||||
:show-last-status="false"
|
||||
:link-to-timeline="false"
|
||||
@deleted="forceLeave"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
<StillImage
|
||||
v-else
|
||||
:src="item.emoji.imageUrl"
|
||||
noStopGifs="true"
|
||||
no-stop-gifs="true"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
<StillImage
|
||||
v-if="suggestion.img"
|
||||
:src="suggestion.img"
|
||||
noStopGifs="true"
|
||||
no-stop-gifs="true"
|
||||
/>
|
||||
<span v-else>{{ suggestion.replacement }}</span>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
<StillImage
|
||||
v-else
|
||||
:src="group.first.imageUrl"
|
||||
noStopGifs="true"
|
||||
no-stop-gifs="true"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -22,13 +22,17 @@ const ListNew = {
|
|||
data () {
|
||||
return {
|
||||
title: '',
|
||||
exclusive: false,
|
||||
userIds: [],
|
||||
selectedUserIds: []
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchList', { id: this.id })
|
||||
.then(() => { this.title = this.findListTitle(this.id) })
|
||||
.then((list) => {
|
||||
this.title = list.title
|
||||
this.exclusive = !!list.exclusive
|
||||
})
|
||||
this.$store.dispatch('fetchListAccounts', { id: this.id })
|
||||
.then(() => {
|
||||
this.selectedUserIds = this.findListAccounts(this.id)
|
||||
|
|
@ -76,7 +80,7 @@ const ListNew = {
|
|||
this.userIds = results
|
||||
},
|
||||
updateList () {
|
||||
this.$store.dispatch('setList', { id: this.id, title: this.title })
|
||||
this.$store.dispatch('setList', { id: this.id, title: this.title, exclusive: this.exclusive })
|
||||
this.$store.dispatch('setListAccounts', { id: this.id, accountIds: this.selectedUserIds })
|
||||
|
||||
this.$router.push({ name: 'list-timeline', params: { id: this.id } })
|
||||
|
|
|
|||
|
|
@ -21,6 +21,17 @@
|
|||
:placeholder="$t('lists.title')"
|
||||
>
|
||||
</div>
|
||||
<div class="input-wrap">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="list-exclusive-input"
|
||||
ref="exclusive"
|
||||
v-model="exclusive"
|
||||
>
|
||||
<label for="list-exclusive-input">
|
||||
{{ $t('lists.exclusive_description') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="member-list">
|
||||
<div
|
||||
v-for="user in selectedUsers"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const ListNew = {
|
|||
data () {
|
||||
return {
|
||||
title: '',
|
||||
exclusive: false,
|
||||
userIds: [],
|
||||
selectedUserIds: []
|
||||
}
|
||||
|
|
@ -67,7 +68,7 @@ const ListNew = {
|
|||
createList () {
|
||||
// the API has two different endpoints for "creating a list with a name"
|
||||
// and "updating the accounts on the list".
|
||||
this.$store.dispatch('createList', { title: this.title })
|
||||
this.$store.dispatch('createList', { title: this.title, exclusive: this.exclusive })
|
||||
.then((list) => {
|
||||
this.$store.dispatch('setListAccounts', { id: list.id, accountIds: this.selectedUserIds })
|
||||
this.$router.push({ name: 'list-timeline', params: { id: list.id } })
|
||||
|
|
|
|||
|
|
@ -21,6 +21,17 @@
|
|||
:placeholder="$t('lists.title')"
|
||||
>
|
||||
</div>
|
||||
<div class="input-wrap">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="list-exclusive-input"
|
||||
ref="exclusive"
|
||||
v-model="exclusive"
|
||||
>
|
||||
<label for="list-exclusive-input">
|
||||
{{ $t('lists.exclusive_description') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="member-list">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ const MediaModal = {
|
|||
},
|
||||
methods: {
|
||||
getType (media) {
|
||||
return fileTypeService.fileType(media.mimetype)
|
||||
return fileTypeService.fileType(media)
|
||||
},
|
||||
hide () {
|
||||
// HACK: Closing immediately via a touch will cause the click
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
:alt="currentMedia.description"
|
||||
:title="currentMedia.description"
|
||||
:image-load-handler="onImageLoaded"
|
||||
noStopGifs="true"
|
||||
no-stop-gifs="true"
|
||||
/>
|
||||
</PinchZoom>
|
||||
</SwipeClick>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import Timeago from 'components/timeago/timeago.vue'
|
||||
import RichContent from 'components/rich_content/rich_content.jsx'
|
||||
import { forEach, map } from 'lodash'
|
||||
import {
|
||||
faCircleCheck,
|
||||
faTriangleExclamation
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
export default {
|
||||
name: 'Poll',
|
||||
|
|
|
|||
|
|
@ -53,6 +53,24 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="poll-hint">
|
||||
<div
|
||||
v-if="poll.akkoma?.anonymous === true"
|
||||
class="alert success"
|
||||
>
|
||||
<FAIcon icon="check-circle" />
|
||||
|
||||
{{ $t('polls.indicate_anonymous') }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="poll.akkoma?.anonymous === false"
|
||||
class="alert warning"
|
||||
>
|
||||
<FAIcon icon="triangle-exclamation" />
|
||||
|
||||
{{ $t('polls.indicate_disclosure') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer faint">
|
||||
<button
|
||||
v-if="!showResults"
|
||||
|
|
@ -144,6 +162,9 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.poll-hint {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
&.loading * {
|
||||
cursor: progress;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -555,7 +555,7 @@ const PostStatusForm = {
|
|||
this.uploadingFiles = false
|
||||
},
|
||||
type (fileInfo) {
|
||||
return fileTypeService.fileType(fileInfo.mimetype)
|
||||
return fileTypeService.fileType(fileInfo)
|
||||
},
|
||||
paste (e) {
|
||||
this.autoPreview()
|
||||
|
|
|
|||
|
|
@ -195,8 +195,8 @@
|
|||
:class="{ 'visibility-tray-edit': isEdit }"
|
||||
>
|
||||
<scope-selector
|
||||
ref="scopeselector"
|
||||
v-if="!disableVisibilitySelector"
|
||||
ref="scopeselector"
|
||||
:user-default="userDefaultScope"
|
||||
:original-scope="copyMessageScope"
|
||||
:initial-scope="newStatus.visibility"
|
||||
|
|
@ -204,10 +204,11 @@
|
|||
/>
|
||||
|
||||
<div
|
||||
class="format-selector-container">
|
||||
class="format-selector-container"
|
||||
>
|
||||
<div
|
||||
class="format-selector"
|
||||
>
|
||||
>
|
||||
<Select
|
||||
id="post-language"
|
||||
v-model="newStatus.language"
|
||||
|
|
|
|||
|
|
@ -7,8 +7,16 @@ const QuoteButton = {
|
|||
name: 'QuoteButton',
|
||||
props: ['status', 'quoting', 'visibility'],
|
||||
computed: {
|
||||
loggedIn () {
|
||||
return !!this.$store.state.users.currentUser
|
||||
showButton () {
|
||||
const currentUserId = this.$store.state.users.currentUser?.id
|
||||
|
||||
if (!currentUserId)
|
||||
return false
|
||||
|
||||
if (['public', 'unlisted', 'local'].includes(this.visibility))
|
||||
return true
|
||||
|
||||
return (this.visibility === 'private' && currentUserId == this.status.user.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="(visibility === 'public' || visibility === 'unlisted') && loggedIn"
|
||||
v-if="showButton"
|
||||
class="QuoteButton"
|
||||
>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ const ProfileTab = {
|
|||
newLocked: this.$store.state.users.currentUser.locked,
|
||||
newPermitFollowback: this.$store.state.users.currentUser.permit_followback,
|
||||
newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })),
|
||||
avatar_description: this.$store.state.users.currentUser.avatar_description || '',
|
||||
header_description: this.$store.state.users.currentUser.header_description || '',
|
||||
pleroma_background_image_description:
|
||||
this.$store.state.users.currentUser.pleroma?.background_image_description || '',
|
||||
showRole: this.$store.state.users.currentUser.show_role,
|
||||
role: this.$store.state.users.currentUser.role,
|
||||
bot: this.$store.state.users.currentUser.bot,
|
||||
|
|
@ -130,15 +134,16 @@ const ProfileTab = {
|
|||
note: this.newBio,
|
||||
locked: this.newLocked,
|
||||
// Backend notation.
|
||||
|
||||
display_name: this.newName,
|
||||
fields_attributes: this.newFields.filter(el => el != null),
|
||||
bot: this.bot,
|
||||
show_role: this.showRole,
|
||||
status_ttl_days: this.expirePosts ? this.newPostTTLDays : -1,
|
||||
permit_followback: this.permit_followback,
|
||||
avatar_description: this.avatar_description,
|
||||
header_description: this.header_description,
|
||||
pleroma_background_image_description: this.pleroma_background_image_description,
|
||||
accepts_direct_messages_from: this.userAcceptsDirectMessagesFrom
|
||||
|
||||
}
|
||||
|
||||
if (this.emailLanguage) {
|
||||
|
|
@ -152,6 +157,17 @@ const ProfileTab = {
|
|||
merge(this.newFields, user.fields)
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
this.$store.dispatch('clearSettingsError')
|
||||
})
|
||||
.catch((error) => {
|
||||
const msg = typeof error === "string" ? error : error.message
|
||||
console.error(`Failed to update profile settings: ${error}`)
|
||||
this.$store.dispatch('setSettingsError', {errorData: msg})
|
||||
this.$store.dispatch('pushGlobalNotice', {
|
||||
messageKey: 'settings.saving_err_details',
|
||||
messageArgs: [msg],
|
||||
level: 'error'
|
||||
})
|
||||
})
|
||||
},
|
||||
changeVis (visibility) {
|
||||
|
|
@ -173,6 +189,7 @@ const ProfileTab = {
|
|||
if (file.size > this.$store.state.instance[slot + 'limit']) {
|
||||
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
|
||||
const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
|
||||
|
||||
this.$store.dispatch('pushGlobalNotice', {
|
||||
messageKey: 'upload.error.message',
|
||||
messageArgs: [
|
||||
|
|
@ -218,10 +235,13 @@ const ProfileTab = {
|
|||
const that = this
|
||||
return new Promise((resolve, reject) => {
|
||||
function updateAvatar (avatar, avatarName) {
|
||||
that.$store.state.api.backendInteractor.updateProfileImages({ avatar, avatarName })
|
||||
that.$store.state.api.backendInteractor.updateProfileImages({
|
||||
avatar, avatarName, avatar_description: this.avatar_description
|
||||
})
|
||||
.then((user) => {
|
||||
that.$store.commit('addNewUsers', [user])
|
||||
that.$store.commit('setCurrentUser', user)
|
||||
this.$store.dispatch('clearSettingsError')
|
||||
resolve()
|
||||
})
|
||||
.catch((error) => {
|
||||
|
|
@ -241,10 +261,13 @@ const ProfileTab = {
|
|||
if (!this.bannerPreview && banner !== '') { return }
|
||||
|
||||
this.bannerUploading = true
|
||||
this.$store.state.api.backendInteractor.updateProfileImages({ banner })
|
||||
this.$store.state.api.backendInteractor.updateProfileImages({
|
||||
banner, header_description: this.header_description
|
||||
})
|
||||
.then((user) => {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
this.$store.commit('setCurrentUser', user)
|
||||
this.$store.dispatch('clearSettingsError')
|
||||
this.bannerPreview = null
|
||||
})
|
||||
.catch(this.displayUploadError)
|
||||
|
|
@ -254,16 +277,21 @@ const ProfileTab = {
|
|||
if (!this.backgroundPreview && background !== '') { return }
|
||||
|
||||
this.backgroundUploading = true
|
||||
this.$store.state.api.backendInteractor.updateProfileImages({ background })
|
||||
this.$store.state.api.backendInteractor.updateProfileImages({
|
||||
background,
|
||||
pleroma_background_image_description: this.pleroma_background_image_description
|
||||
})
|
||||
.then((data) => {
|
||||
this.$store.commit('addNewUsers', [data])
|
||||
this.$store.commit('setCurrentUser', data)
|
||||
this.$store.dispatch('clearSettingsError')
|
||||
this.backgroundPreview = null
|
||||
})
|
||||
.catch(this.displayUploadError)
|
||||
.finally(() => { this.backgroundUploading = false })
|
||||
},
|
||||
displayUploadError (error) {
|
||||
this.$store.dispatch('setSettingsError', {errorData: error})
|
||||
this.$store.dispatch('pushGlobalNotice', {
|
||||
messageKey: 'upload.error.message',
|
||||
messageArgs: [error.message],
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.setting-item .media-alt {
|
||||
margin-bottom: 0.75em;
|
||||
height: 2.5em;
|
||||
}
|
||||
|
||||
.expire-posts-days {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,21 @@
|
|||
:set-language="val => emailLanguage = val"
|
||||
/>
|
||||
</p>
|
||||
<h3>{{ $t('settings.media_alt_avatar') }}</h3>
|
||||
<textarea
|
||||
v-model="avatar_description"
|
||||
class="media-alt resize-height"
|
||||
/>
|
||||
<h3>{{ $t('settings.media_alt_banner') }}</h3>
|
||||
<textarea
|
||||
v-model="header_description"
|
||||
class="media-alt resize-height"
|
||||
/>
|
||||
<h3>{{ $t('settings.media_alt_background') }}</h3>
|
||||
<textarea
|
||||
v-model="pleroma_background_image_description"
|
||||
class="media-alt resize-height"
|
||||
/>
|
||||
<button
|
||||
:disabled="newName && newName.length === 0"
|
||||
class="btn button-default"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
faHome,
|
||||
faComments,
|
||||
faBolt,
|
||||
faBookmark,
|
||||
faUserPlus,
|
||||
faBullhorn,
|
||||
faSearch,
|
||||
|
|
@ -25,6 +26,7 @@ library.add(
|
|||
faHome,
|
||||
faComments,
|
||||
faBolt,
|
||||
faBookmark,
|
||||
faUserPlus,
|
||||
faBullhorn,
|
||||
faSearch,
|
||||
|
|
|
|||
|
|
@ -85,6 +85,18 @@
|
|||
/> {{ $t("nav.lists") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li
|
||||
v-if="currentUser"
|
||||
@click="toggleDrawer"
|
||||
>
|
||||
<router-link :to="{ name: 'bookmarks' }">
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="bookmark"
|
||||
/> {{ $t("nav.bookmarks") }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-if="currentUser">
|
||||
<li @click="toggleDrawer">
|
||||
|
|
|
|||
|
|
@ -99,19 +99,21 @@
|
|||
<router-link
|
||||
v-else
|
||||
:to="retweeterProfileLink"
|
||||
>{{ retweeter }}</router-link>
|
||||
>
|
||||
{{ retweeter }}
|
||||
</router-link>
|
||||
</div>
|
||||
{{ ' ' }}
|
||||
|
||||
<div
|
||||
class="repeat-tooltip"
|
||||
>
|
||||
<FAIcon
|
||||
icon="retweet"
|
||||
class="repeat-icon"
|
||||
:title="$t('tool_tip.repeat')"
|
||||
/>
|
||||
{{ $t('timeline.repeated') }}
|
||||
<FAIcon
|
||||
icon="retweet"
|
||||
class="repeat-icon"
|
||||
:title="$t('tool_tip.repeat')"
|
||||
/>
|
||||
{{ $t('timeline.repeated') }}
|
||||
</div>
|
||||
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ const StatusContent = {
|
|||
return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
|
||||
},
|
||||
attachmentTypes () {
|
||||
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
|
||||
return this.status.attachments.map(file => fileType.fileType(file))
|
||||
},
|
||||
translationLanguages () {
|
||||
return (this.$store.state.instance.supportedTranslationLanguages.source || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name }))
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const StillImage = {
|
|||
'imageLoadError',
|
||||
'imageLoadHandler',
|
||||
'alt',
|
||||
'title',
|
||||
'height',
|
||||
'width',
|
||||
'noStopGifs'
|
||||
|
|
@ -27,6 +28,16 @@ const StillImage = {
|
|||
animated () {
|
||||
return this.stopGifs && this.isAnimated
|
||||
},
|
||||
titleText () {
|
||||
return this.title || this.alt
|
||||
},
|
||||
ariaLabel () {
|
||||
// if the title (a UI hint) differs from the alt text (describing image content),
|
||||
// we wat to add an aria-label to pass along the UI hint to screen readers
|
||||
if (this.title && this.alt != this.title)
|
||||
return this.title
|
||||
return undefined
|
||||
},
|
||||
style () {
|
||||
const appendPx = (str) => /\d$/.test(str) ? str + 'px' : str
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
class="still-image"
|
||||
:class="{ animated: animated, pixelart: isPixelArt }"
|
||||
:style="style"
|
||||
:aria-label="ariaLabel"
|
||||
>
|
||||
<div
|
||||
v-if="animated && imageTypeLabel"
|
||||
|
|
@ -20,7 +21,7 @@
|
|||
ref="src"
|
||||
:key="src"
|
||||
:alt="alt"
|
||||
:title="alt"
|
||||
:title="titleText"
|
||||
:src="src"
|
||||
:referrerpolicy="referrerpolicy"
|
||||
@load="onLoad"
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@
|
|||
v-if="$slots.extraHeading"
|
||||
class="timeline-extra-heading"
|
||||
>
|
||||
<slot name="extraHeading"></slot>
|
||||
<slot name="extraHeading" />
|
||||
</div>
|
||||
<div :class="classes.body">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<StillImage
|
||||
v-if="user"
|
||||
class="avatar"
|
||||
:alt="user.screen_name_ui"
|
||||
:alt="user.avatar_description || user.screen_name_ui"
|
||||
:title="user.screen_name_ui"
|
||||
:src="imgSrc(user.profile_image_url_original)"
|
||||
:image-load-error="imageLoadError"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { mapGetters } from 'vuex'
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faBell,
|
||||
faImages,
|
||||
faRss,
|
||||
faSearchPlus,
|
||||
faExternalLinkAlt,
|
||||
|
|
@ -21,6 +22,7 @@ import {
|
|||
library.add(
|
||||
faRss,
|
||||
faBell,
|
||||
faImages,
|
||||
faSearchPlus,
|
||||
faExternalLinkAlt,
|
||||
faEdit
|
||||
|
|
@ -28,7 +30,7 @@ library.add(
|
|||
|
||||
export default {
|
||||
props: [
|
||||
'userId', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar'
|
||||
'userId', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar', 'showMediaButton'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
|
|
@ -122,6 +124,10 @@ export default {
|
|||
&& (this.$store.state.interface.layoutType !== 'mobile')
|
||||
&& this.switcher
|
||||
},
|
||||
hasProfileMedia() {
|
||||
const user = this.user
|
||||
return (user && (user.profile_image_url_original || user.cover_photo || user.background_image))
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
components: {
|
||||
|
|
@ -191,11 +197,51 @@ export default {
|
|||
zoomAvatar () {
|
||||
const attachment = {
|
||||
url: this.user.profile_image_url_original,
|
||||
description: this.user.avatar_description,
|
||||
mimetype: 'image'
|
||||
}
|
||||
this.$store.dispatch('setMedia', [attachment])
|
||||
this.$store.dispatch('setCurrentMedia', attachment)
|
||||
},
|
||||
makeMediaObject(link, altText) {
|
||||
// Pseudo media attachment object with just the fields relevant for gallery display
|
||||
return {
|
||||
type: "image",
|
||||
mimetype: "image/something",
|
||||
url: link,
|
||||
// proper media attachments omit field if no alt, but Mastodon decided in
|
||||
// user responses to return empty string for "no alt". Normalise this here
|
||||
description: altText || undefined
|
||||
}
|
||||
},
|
||||
showProfileMedia () {
|
||||
const user = this.user
|
||||
const media = []
|
||||
let startMedia = undefined
|
||||
|
||||
if (user.profile_image_url_original) {
|
||||
const avatar = this.makeMediaObject(user.profile_image_url_original, user.avatar_description)
|
||||
media.push(avatar)
|
||||
if (!startMedia) startMedia = avatar
|
||||
}
|
||||
|
||||
if (user.cover_photo) {
|
||||
const header = this.makeMediaObject(user.cover_photo, user.header_description)
|
||||
media.push(header)
|
||||
if (!startMedia) startMedia = header
|
||||
}
|
||||
|
||||
if (user.background_image) {
|
||||
const background = this.makeMediaObject(user.background_image, user.pleroma?.background_image_description)
|
||||
media.push(background)
|
||||
if (!startMedia) startMedia = background
|
||||
}
|
||||
|
||||
if (startMedia) {
|
||||
this.$store.dispatch('setMedia', media)
|
||||
this.$store.dispatch('setCurrentMedia', startMedia)
|
||||
}
|
||||
},
|
||||
mentionUser () {
|
||||
this.$store.dispatch('openPostStatusModal', { repliedUser: this.user })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@
|
|||
justify-self: end;
|
||||
}
|
||||
|
||||
.user-profile-media-button {
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
word-wrap: break-word;
|
||||
border-bottom-right-radius: inherit;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<div class="panel-heading -flexible-height">
|
||||
<div
|
||||
class="user-info"
|
||||
:class="{ '-compact': this.compactUserInfo }"
|
||||
:class="{ '-compact': compactUserInfo }"
|
||||
>
|
||||
<div class="container">
|
||||
<a
|
||||
|
|
@ -54,7 +54,10 @@
|
|||
>
|
||||
@{{ user.screen_name_ui }}
|
||||
</router-link>
|
||||
<span class="user-roles" v-if="!hideBio && (user.deactivated || !!visibleRole || user.bot)">
|
||||
<span
|
||||
v-if="!hideBio && (user.deactivated || !!visibleRole || user.bot)"
|
||||
class="user-roles"
|
||||
>
|
||||
<span
|
||||
v-if="user.deactivated"
|
||||
class="alert user-role"
|
||||
|
|
@ -74,7 +77,10 @@
|
|||
{{ $t('user_card.bot') }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="user-locked" v-if="user.locked">
|
||||
<span
|
||||
v-if="user.locked"
|
||||
class="user-locked"
|
||||
>
|
||||
<FAIcon
|
||||
class="lock-icon"
|
||||
icon="lock"
|
||||
|
|
@ -114,44 +120,44 @@
|
|||
</div>
|
||||
<div class="user-buttons">
|
||||
<button
|
||||
v-if="!isOtherUser && user.is_local"
|
||||
class="button-unstyled edit-profile-button"
|
||||
@click.stop="openProfileTab"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="icon"
|
||||
icon="edit"
|
||||
:title="$t('user_card.edit_profile')"
|
||||
/>
|
||||
</button>
|
||||
<a
|
||||
v-if="isOtherUser && !user.is_local"
|
||||
:href="user.statusnet_profile_url"
|
||||
target="_blank"
|
||||
class="button-unstyled external-link-button"
|
||||
>
|
||||
<FAIcon
|
||||
class="icon"
|
||||
icon="external-link-alt"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
v-if="isOtherUser"
|
||||
:href="user.statusnet_profile_url + '.rss'"
|
||||
target="_blank"
|
||||
class="button-unstyled external-link-button"
|
||||
>
|
||||
<FAIcon
|
||||
class="icon"
|
||||
icon="rss"
|
||||
/>
|
||||
</a>
|
||||
<AccountActions
|
||||
v-if="isOtherUser && loggedIn"
|
||||
:user="user"
|
||||
:relationship="relationship"
|
||||
v-if="!isOtherUser && user.is_local"
|
||||
class="button-unstyled edit-profile-button"
|
||||
@click.stop="openProfileTab"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="icon"
|
||||
icon="edit"
|
||||
:title="$t('user_card.edit_profile')"
|
||||
/>
|
||||
</button>
|
||||
<a
|
||||
v-if="isOtherUser && !user.is_local"
|
||||
:href="user.statusnet_profile_url"
|
||||
target="_blank"
|
||||
class="button-unstyled external-link-button"
|
||||
>
|
||||
<FAIcon
|
||||
class="icon"
|
||||
icon="external-link-alt"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
v-if="isOtherUser"
|
||||
:href="user.statusnet_profile_url + '.rss'"
|
||||
target="_blank"
|
||||
class="button-unstyled external-link-button"
|
||||
>
|
||||
<FAIcon
|
||||
class="icon"
|
||||
icon="rss"
|
||||
/>
|
||||
</a>
|
||||
<AccountActions
|
||||
v-if="isOtherUser && loggedIn"
|
||||
:user="user"
|
||||
:relationship="relationship"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-meta">
|
||||
|
|
@ -212,6 +218,23 @@
|
|||
</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div
|
||||
class="user-profile-media-button"
|
||||
v-if="showMediaButton"
|
||||
>
|
||||
<button
|
||||
class="btn button-default"
|
||||
:title="$i18n.t('user_card.show_profile_media')"
|
||||
:aria-label="$i18n.t('user_card.show_profile_media')"
|
||||
:disabled="!hasProfileMedia"
|
||||
@click="showProfileMedia"
|
||||
>
|
||||
<FAIcon
|
||||
icon="images"
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="loggedIn && isOtherUser"
|
||||
|
|
@ -303,7 +326,7 @@
|
|||
:html="user.description_html"
|
||||
:emoji="user.emoji"
|
||||
:handle-links="true"
|
||||
:style='{"text-align": this.$store.getters.mergedConfig.centerAlignBio ? "center" : "start"}'
|
||||
:style='{"text-align": $store.getters.mergedConfig.centerAlignBio ? "center" : "start"}'
|
||||
/>
|
||||
</div>
|
||||
<teleport to="#modal">
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
:switcher="true"
|
||||
:selected="timeline.viewing"
|
||||
:allow-zooming-avatar="true"
|
||||
:show-media-button="true"
|
||||
rounded="top"
|
||||
/>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@
|
|||
"lists": {
|
||||
"create": "Create",
|
||||
"delete": "Delete list",
|
||||
"exclusive_description": "Remove users on list from home timeline",
|
||||
"following_only": "Limit to Following",
|
||||
"lists": "Lists",
|
||||
"new": "New List",
|
||||
|
|
@ -382,6 +383,8 @@
|
|||
"expired": "Poll ended {0} ago",
|
||||
"expires_in": "Poll ends in {0}",
|
||||
"expiry": "Poll age",
|
||||
"indicate_disclosure": "Source instance will publish voter indentity and their votes",
|
||||
"indicate_anonymous": "Source instance pledged to keep voter identity anonymous",
|
||||
"multiple_choices": "Multiple choices",
|
||||
"not_enough_options": "Too few unique options in poll",
|
||||
"option": "Option",
|
||||
|
|
@ -634,6 +637,9 @@
|
|||
"mascot": "Mastodon FE Mascot",
|
||||
"max_depth_in_thread": "Maximum number of levels in thread to display by default",
|
||||
"max_thumbnails": "Maximum amount of thumbnails per post (empty = no limit)",
|
||||
"media_alt_avatar": "Avatar alt text",
|
||||
"media_alt_background": "Background alt text",
|
||||
"media_alt_banner": "Banner alt text",
|
||||
"mention_link_bolden_you": "Highlight mention of you when you are mentioned",
|
||||
"mention_link_display": "Display mention links",
|
||||
"mention_link_display_full": "always as full names (e.g. {'@'}foo{'@'}example.org)",
|
||||
|
|
@ -746,6 +752,7 @@
|
|||
"right_sidebar": "Reverse order of columns",
|
||||
"save": "Save changes",
|
||||
"saving_err": "Error saving settings",
|
||||
"saving_err_details": "Error saving settings: {0}",
|
||||
"saving_ok": "Settings saved",
|
||||
"scope_copy": "Copy scope when replying (DMs are always copied)",
|
||||
"search_user_to_block": "Search whom you want to block",
|
||||
|
|
@ -1208,6 +1215,7 @@
|
|||
"replies": "With Replies",
|
||||
"report": "Report",
|
||||
"requested_by": "Has requested to follow you",
|
||||
"show_profile_media": "Show profile images and their alt text",
|
||||
"show_repeats": "Show repeats",
|
||||
"statuses": "Posts",
|
||||
"subscribe": "Subscribe",
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ const interfaceMod = {
|
|||
state.settings.currentSaveStateNotice = { error: true, errorData: error }
|
||||
}
|
||||
},
|
||||
setSettingsError(state, { errorData }) {
|
||||
state.settings.currentSaveStateNotice = { error: true, errorData: errorData }
|
||||
},
|
||||
clearSettingsError(state) {
|
||||
state.settings.currentSaveStateNotice = { error: false, data: "manual reset" }
|
||||
},
|
||||
setNotificationPermission (state, permission) {
|
||||
state.notificationPermission = permission
|
||||
},
|
||||
|
|
@ -113,6 +119,12 @@ const interfaceMod = {
|
|||
settingsSaved ({ commit, dispatch }, { success, error }) {
|
||||
commit('settingsSaved', { success, error })
|
||||
},
|
||||
setSettingsError ({ commit }, { errorData }) {
|
||||
commit('setSettingsError', { errorData })
|
||||
},
|
||||
clearSettingsError ({ commit }) {
|
||||
commit('clearSettingsError')
|
||||
},
|
||||
setNotificationPermission ({ commit }, permission) {
|
||||
commit('setNotificationPermission', permission)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,22 +8,34 @@ export const defaultState = {
|
|||
export const mutations = {
|
||||
setLists (state, value) {
|
||||
state.allLists = value
|
||||
|
||||
// Stub out fields for newly fetched lists
|
||||
for (const list of state.allLists) {
|
||||
if (!list.accountIds) list.accountIds = []
|
||||
}
|
||||
},
|
||||
setList (state, { id, title }) {
|
||||
setList (state, { id, title, exclusive }) {
|
||||
if (!state.allListsObject[id]) {
|
||||
state.allListsObject[id] = {}
|
||||
}
|
||||
state.allListsObject[id].title = title
|
||||
|
||||
if (!find(state.allLists, { id })) {
|
||||
state.allLists.push({ id, title })
|
||||
const list = state.allListsObject[id]
|
||||
list.title = title
|
||||
list.exclusive = exclusive
|
||||
// newly created list
|
||||
if (!list.accountIds) list.accountIds = []
|
||||
|
||||
const listEntry = find(state.allLists, { id })
|
||||
if (!listEntry) {
|
||||
state.allLists.push({ id, ...list })
|
||||
} else {
|
||||
find(state.allLists, { id }).title = title
|
||||
Object.assign(listEntry, list)
|
||||
}
|
||||
},
|
||||
setListAccounts (state, { id, accountIds }) {
|
||||
// XXX: this shouldn’t happen in the first place...
|
||||
if (!state.allListsObject[id]) {
|
||||
state.allListsObject[id] = {}
|
||||
state.allListsObject[id] = { title: "", exclusive: false }
|
||||
}
|
||||
state.allListsObject[id].accountIds = accountIds
|
||||
},
|
||||
|
|
@ -37,24 +49,27 @@ const actions = {
|
|||
setLists ({ commit }, value) {
|
||||
commit('setLists', value)
|
||||
},
|
||||
createList ({ rootState, commit }, { title }) {
|
||||
return rootState.api.backendInteractor.createList({ title })
|
||||
createList ({ rootState, commit }, { title, exclusive }) {
|
||||
return rootState.api.backendInteractor.createList({ title, exclusive })
|
||||
.then((list) => {
|
||||
commit('setList', { id: list.id, title })
|
||||
commit('setList', list)
|
||||
return list
|
||||
})
|
||||
},
|
||||
fetchList ({ rootState, commit }, { id }) {
|
||||
return rootState.api.backendInteractor.getList({ id })
|
||||
.then((list) => commit('setList', { id: list.id, title: list.title }))
|
||||
.then((list) => {
|
||||
commit('setList', list)
|
||||
return list
|
||||
})
|
||||
},
|
||||
fetchListAccounts ({ rootState, commit }, { id }) {
|
||||
return rootState.api.backendInteractor.getListAccounts({ id })
|
||||
.then((accountIds) => commit('setListAccounts', { id, accountIds }))
|
||||
},
|
||||
setList ({ rootState, commit }, { id, title }) {
|
||||
rootState.api.backendInteractor.updateList({ id, title })
|
||||
commit('setList', { id, title })
|
||||
setList ({ rootState, commit }, { id, title, exclusive }) {
|
||||
rootState.api.backendInteractor.updateList({ id, title, exclusive })
|
||||
.then((list) => commit('setList', list))
|
||||
},
|
||||
setListAccounts ({ rootState, commit }, { id, accountIds }) {
|
||||
const saved = rootState.lists.allListsObject[id].accountIds
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const mediaViewer = {
|
|||
actions: {
|
||||
setMedia ({ commit }, attachments) {
|
||||
const media = attachments.filter(attachment => {
|
||||
const type = fileTypeService.fileType(attachment.mimetype)
|
||||
const type = fileTypeService.fileType(attachment)
|
||||
return supportedTypes.has(type)
|
||||
})
|
||||
commit('setMedia', media)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { merge } from 'lodash'
|
||||
|
||||
const POLL_UPDATE_FREQUENCY = 150_000;
|
||||
|
||||
const polls = {
|
||||
state: {
|
||||
// Contains key = id, value = number of trackers for this poll
|
||||
|
|
@ -12,6 +14,9 @@ const polls = {
|
|||
// Make expired-state change trigger re-renders properly
|
||||
poll.expired = Date.now() > Date.parse(poll.expires_at)
|
||||
if (existingPoll) {
|
||||
if (poll.expired) {
|
||||
state.trackedPolls[poll.id] = 0
|
||||
}
|
||||
state.pollsObject[poll.id] = merge(existingPoll, poll)
|
||||
} else {
|
||||
state.pollsObject[poll.id] = poll
|
||||
|
|
@ -44,13 +49,16 @@ const polls = {
|
|||
if (rootState.polls.trackedPolls[pollId]) {
|
||||
dispatch('updateTrackedPoll', pollId)
|
||||
}
|
||||
}, 30 * 1000)
|
||||
}, POLL_UPDATE_FREQUENCY)
|
||||
commit('mergeOrAddPoll', poll)
|
||||
})
|
||||
},
|
||||
trackPoll ({ rootState, commit, dispatch }, pollId) {
|
||||
if (rootState.polls.pollsObject[pollId]?.expired)
|
||||
return;
|
||||
|
||||
if (!rootState.polls.trackedPolls[pollId]) {
|
||||
setTimeout(() => dispatch('updateTrackedPoll', pollId), 30 * 1000)
|
||||
setTimeout(() => dispatch('updateTrackedPoll', pollId), POLL_UPDATE_FREQUENCY)
|
||||
}
|
||||
commit('trackPoll', pollId)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -31,12 +31,12 @@ const mergeArrayLength = (oldValue, newValue) => {
|
|||
}
|
||||
}
|
||||
|
||||
const getNotificationPermission = () => {
|
||||
const getNotificationPermission = async () => {
|
||||
const Notification = window.Notification
|
||||
|
||||
if (!Notification) return Promise.resolve(null)
|
||||
if (Notification.permission === 'default') return Notification.requestPermission()
|
||||
return Promise.resolve(Notification.permission)
|
||||
if (!Notification) return null
|
||||
if (Notification.permission === 'default') return await Notification.requestPermission()
|
||||
return Notification.permission
|
||||
}
|
||||
|
||||
const blockUser = (store, id) => {
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ const updateNotificationSettings = ({ credentials, settings }) => {
|
|||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const updateProfileImages = ({ credentials, avatar = null, avatarName = null, banner = null, background = null }) => {
|
||||
const updateProfileImages = ({ credentials, avatar = null, avatarName = null, banner = null, background = null, ...extra }) => {
|
||||
const form = new FormData()
|
||||
if (avatar !== null) {
|
||||
if (avatarName !== null) {
|
||||
|
|
@ -202,6 +202,10 @@ const updateProfileImages = ({ credentials, avatar = null, avatarName = null, ba
|
|||
}
|
||||
if (banner !== null) form.append('header', banner)
|
||||
if (background !== null) form.append('pleroma_background_image', background)
|
||||
|
||||
for (const ekey in extra)
|
||||
form.append(ekey, extra[ekey])
|
||||
|
||||
return fetch(MASTODON_PROFILE_UPDATE_URL, {
|
||||
headers: authHeaders(credentials),
|
||||
method: 'PATCH',
|
||||
|
|
@ -426,7 +430,7 @@ const fetchLists = ({ credentials }) => {
|
|||
.then((data) => data.json())
|
||||
}
|
||||
|
||||
const createList = ({ title, credentials }) => {
|
||||
const createList = ({ title, exclusive, credentials }) => {
|
||||
const url = MASTODON_LISTS_URL
|
||||
const headers = authHeaders(credentials)
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
|
@ -434,7 +438,7 @@ const createList = ({ title, credentials }) => {
|
|||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({ title })
|
||||
body: JSON.stringify({ title, exclusive })
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
|
|
@ -444,7 +448,7 @@ const getList = ({ id, credentials }) => {
|
|||
.then((data) => data.json())
|
||||
}
|
||||
|
||||
const updateList = ({ id, title, credentials }) => {
|
||||
const updateList = ({ id, title, exclusive, credentials }) => {
|
||||
const url = MASTODON_LIST_URL(id)
|
||||
const headers = authHeaders(credentials)
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
|
@ -452,8 +456,8 @@ const updateList = ({ id, title, credentials }) => {
|
|||
return fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: headers,
|
||||
body: JSON.stringify({ title })
|
||||
})
|
||||
body: JSON.stringify({ title, exclusive })
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const getListAccounts = ({ id, credentials }) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,17 @@
|
|||
export const showDesktopNotification = (rootState, desktopNotificationOpts) => {
|
||||
export const showDesktopNotification = async (rootState, desktopNotificationOpts) => {
|
||||
if (!('Notification' in window && window.Notification.permission === 'granted')) return
|
||||
if (rootState.statuses.notifications.desktopNotificationSilence) { return }
|
||||
|
||||
// we in mobile?
|
||||
if ('serviceWorker' in navigator) {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
return registration.showNotification(desktopNotificationOpts.title, desktopNotificationOpts);
|
||||
} catch (err) {
|
||||
console.error("Service Worker failed me:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// or maybe not?
|
||||
return new window.Notification(desktopNotificationOpts.title, desktopNotificationOpts)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,9 +83,11 @@ export const parseUser = (data) => {
|
|||
// Utilize avatar_static for gif avatars?
|
||||
output.profile_image_url = data.avatar
|
||||
output.profile_image_url_original = data.avatar
|
||||
output.avatar_description = data.avatar_description
|
||||
|
||||
// Same, utilize header_static?
|
||||
output.cover_photo = data.header
|
||||
output.header_description = data.header_description
|
||||
|
||||
output.friends_count = data.following_count
|
||||
|
||||
|
|
@ -104,6 +106,8 @@ export const parseUser = (data) => {
|
|||
const relationship = data.pleroma.relationship
|
||||
|
||||
output.background_image = data.pleroma.background_image
|
||||
output.pleroma.background_image_description = data.pleroma.background_image_description
|
||||
|
||||
output.favicon = data.pleroma.favicon
|
||||
|
||||
output.pleroma.unread_conversation_count = data.pleroma.unread_conversation_count
|
||||
|
|
@ -243,8 +247,10 @@ export const parseAttachment = (data) => {
|
|||
output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type
|
||||
output.meta = data.meta
|
||||
output.id = data.id
|
||||
output.type = data.type
|
||||
} else {
|
||||
output.mimetype = data.mimetype
|
||||
output.type = 'unknown'
|
||||
// output.meta = ??? missing
|
||||
}
|
||||
|
||||
|
|
@ -306,6 +312,8 @@ export const parseStatus = (data) => {
|
|||
|
||||
if (data.akkoma) {
|
||||
const { akkoma } = data
|
||||
output.akkoma = akkoma
|
||||
|
||||
if (akkoma && akkoma.source) {
|
||||
output.media_type = akkoma.source.mediaType
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// TODO this func might as well take the entire file and use its mimetype
|
||||
// or the entire service could be just mimetype service that only operates
|
||||
// on mimetypes and not files. Currently the naming is confusing.
|
||||
const fileType = mimetype => {
|
||||
const fileType = mediaAttachment => {
|
||||
// *oma extension; full MIME type of the file if known
|
||||
const mimetype = mediaAttachment.mimetype
|
||||
|
||||
if (mimetype.match(/flash/)) {
|
||||
return 'flash'
|
||||
}
|
||||
|
|
@ -22,15 +22,16 @@ const fileType = mimetype => {
|
|||
return 'audio'
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
// if inconclusive, try generic type from vanilla Mastodon
|
||||
if (mediaAttachment.type === 'gifv') {
|
||||
return 'video'
|
||||
}
|
||||
|
||||
const fileMatchesSomeType = (types, file) =>
|
||||
types.some(type => fileType(file.mimetype) === type)
|
||||
return (mediaAttachment.type || 'unknown')
|
||||
}
|
||||
|
||||
const fileTypeService = {
|
||||
fileType,
|
||||
fileMatchesSomeType
|
||||
}
|
||||
|
||||
export default fileTypeService
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export const getAttrs = tag => {
|
|||
.replace(new RegExp('^' + getTagName(tag)), '')
|
||||
.replace(/\/?$/, '')
|
||||
.trim()
|
||||
const attrs = Array.from(innertag.matchAll(/([a-z0-9-]+)(?:=("[^"]+?"|'[^']+?'))?/gi))
|
||||
const attrs = Array.from(innertag.matchAll(/([a-z]+[a-z0-9-]*)(?:=((?:"(?:\\.|[^"\\])*")|(?:'(?:\\.|[^'\\])*')))?/gi))
|
||||
.map(([trash, key, value]) => [key, value])
|
||||
.map(([k, v]) => {
|
||||
if (!v) return [k, true]
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ describe('RichContent', () => {
|
|||
const html = p('Ebin :DDDD :spurdo:')
|
||||
const expected = p(
|
||||
'Ebin :DDDD ',
|
||||
'<anonymous-stub src="about:blank" alt=":spurdo:" class="emoji img" title=":spurdo:"></anonymous-stub>'
|
||||
'<anonymous-stub src="about:blank" title=":spurdo:" alt=":spurdo:" class="emoji img"></anonymous-stub>'
|
||||
)
|
||||
|
||||
const wrapper = shallowMount(RichContent, {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ describe('The lists module', () => {
|
|||
describe('mutations', () => {
|
||||
it('updates array of all lists', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const list = { id: '1', title: 'testList' }
|
||||
const list = { id: '1', title: 'testList', exclusive: false }
|
||||
|
||||
mutations.setLists(state, [list])
|
||||
expect(state.allLists).to.have.length(1)
|
||||
|
|
@ -14,37 +14,51 @@ describe('The lists module', () => {
|
|||
|
||||
it('adds a new list with a title, updating the title for existing lists', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const list = { id: '1', title: 'testList' }
|
||||
const modList = { id: '1', title: 'anotherTestTitle' }
|
||||
|
||||
mutations.setList(state, list)
|
||||
expect(state.allListsObject[list.id]).to.eql({ title: list.title })
|
||||
expect(state.allLists).to.have.length(1)
|
||||
expect(state.allLists[0]).to.eql(list)
|
||||
// new list
|
||||
const listInit = { id: '1', title: 'testList', exclusive: false }
|
||||
const listResultObj = { title: 'testList', exclusive: false, accountIds: [] }
|
||||
const listResultItem = { id: '1', ...listResultObj }
|
||||
|
||||
mutations.setList(state, modList)
|
||||
expect(state.allListsObject[modList.id]).to.eql({ title: modList.title })
|
||||
mutations.setList(state, listInit)
|
||||
expect(state.allListsObject[listInit.id]).to.eql(listResultObj)
|
||||
expect(state.allLists).to.have.length(1)
|
||||
expect(state.allLists[0]).to.eql(modList)
|
||||
expect(state.allLists[0]).to.eql(listResultItem)
|
||||
|
||||
// update
|
||||
const modParams = { id: '1', title: 'anotherTestTitle', exclusive: true }
|
||||
const modResultObj = { title: 'anotherTestTitle', exclusive: true, accountIds: [] }
|
||||
const modResultItem = { id: '1', ...modResultObj }
|
||||
|
||||
mutations.setList(state, modParams)
|
||||
expect(state.allListsObject[modParams.id]).to.eql(modResultObj)
|
||||
expect(state.allLists).to.have.length(1)
|
||||
expect(state.allLists[0]).to.eql(modResultItem)
|
||||
})
|
||||
|
||||
it('adds a new list with an array of IDs, updating the IDs for existing lists', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
|
||||
// new list
|
||||
const list = { id: '1', accountIds: ['1', '2', '3'] }
|
||||
const modList = { id: '1', accountIds: ['3', '4', '5'] }
|
||||
|
||||
mutations.setListAccounts(state, list)
|
||||
expect(state.allListsObject[list.id]).to.eql({ accountIds: list.accountIds })
|
||||
expect(state.allListsObject[list.id]).to.eql({ accountIds: list.accountIds, title: "", exclusive: false })
|
||||
|
||||
mutations.setListAccounts(state, modList)
|
||||
expect(state.allListsObject[modList.id]).to.eql({ accountIds: modList.accountIds })
|
||||
// update, preserving title etc
|
||||
const title = "FunList"
|
||||
const exclusive = true
|
||||
mutations.setList(state, { id: list.id, title, exclusive })
|
||||
|
||||
const modParams = { id: list.id, accountIds: ['3', '4', '5'] }
|
||||
mutations.setListAccounts(state, modParams)
|
||||
expect(state.allListsObject[modParams.id]).to.eql({ accountIds: modParams.accountIds, title, exclusive })
|
||||
})
|
||||
|
||||
it('deletes a list', () => {
|
||||
const state = {
|
||||
allLists: [{ id: '1', title: 'testList' }],
|
||||
allLists: [{ id: '1', title: 'testList', exclusive: false }],
|
||||
allListsObject: {
|
||||
1: { title: 'testList', accountIds: ['1', '2', '3'] }
|
||||
1: { title: 'testList', accountIds: ['1', '2', '3'], exclusive: false }
|
||||
}
|
||||
}
|
||||
const id = '1'
|
||||
|
|
@ -58,9 +72,9 @@ describe('The lists module', () => {
|
|||
describe('getters', () => {
|
||||
it('returns list title', () => {
|
||||
const state = {
|
||||
allLists: [{ id: '1', title: 'testList' }],
|
||||
allLists: [{ id: '1', title: 'testList', exclusive: false }],
|
||||
allListsObject: {
|
||||
1: { title: 'testList', accountIds: ['1', '2', '3'] }
|
||||
1: { title: 'testList', accountIds: ['1', '2', '3'], exclusive: false }
|
||||
}
|
||||
}
|
||||
const id = '1'
|
||||
|
|
@ -70,9 +84,9 @@ describe('The lists module', () => {
|
|||
|
||||
it('returns list accounts', () => {
|
||||
const state = {
|
||||
allLists: [{ id: '1', title: 'testList' }],
|
||||
allLists: [{ id: '1', title: 'testList', exclusive: false }],
|
||||
allListsObject: {
|
||||
1: { title: 'testList', accountIds: ['1', '2', '3'] }
|
||||
1: { title: 'testList', accountIds: ['1', '2', '3'], exclusive: false }
|
||||
}
|
||||
}
|
||||
const id = '1'
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
import fileType from 'src/services/file_type/file_type.service.js'
|
||||
|
||||
describe('fileType service', () => {
|
||||
describe('fileMatchesSomeType', () => {
|
||||
it('should be true when file type is one of the listed', () => {
|
||||
const file = { mimetype: 'audio/mpeg' }
|
||||
const types = ['video', 'audio']
|
||||
|
||||
expect(fileType.fileMatchesSomeType(types, file)).to.eql(true)
|
||||
})
|
||||
|
||||
it('should be false when files type is not included in type list', () => {
|
||||
const file = { mimetype: 'audio/mpeg' }
|
||||
const types = ['image', 'video']
|
||||
|
||||
expect(fileType.fileMatchesSomeType(types, file)).to.eql(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue