Merge branch 'more-fixes' into mfm
This commit is contained in:
commit
5ca430a1c0
358 changed files with 11941 additions and 10638 deletions
|
|
@ -1,10 +1,10 @@
|
|||
import { mapState } from 'pinia'
|
||||
|
||||
import FeaturesPanel from '../features_panel/features_panel.vue'
|
||||
import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue'
|
||||
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
|
||||
import StaffPanel from '../staff_panel/staff_panel.vue'
|
||||
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
|
||||
import FeaturesPanel from 'src/components/features_panel/features_panel.vue'
|
||||
import InstanceSpecificPanel from 'src/components/instance_specific_panel/instance_specific_panel.vue'
|
||||
import MRFTransparencyPanel from 'src/components/mrf_transparency_panel/mrf_transparency_panel.vue'
|
||||
import StaffPanel from 'src/components/staff_panel/staff_panel.vue'
|
||||
import TermsOfServicePanel from 'src/components/terms_of_service_panel/terms_of_service_panel.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
|
@ -27,7 +27,11 @@ const About = {
|
|||
frontendVersionLink() {
|
||||
return pleromaFeCommitUrl + this.frontendVersion
|
||||
},
|
||||
...mapState(useInstanceStore, ['backendVersion', 'backendRepository', 'frontendVersion']),
|
||||
...mapState(useInstanceStore, [
|
||||
'backendVersion',
|
||||
'backendRepository',
|
||||
'frontendVersion',
|
||||
]),
|
||||
showInstanceSpecificPanel() {
|
||||
return (
|
||||
useInstanceStore().instanceIdentity.showInstanceSpecificPanel &&
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@
|
|||
<features-panel v-if="showFeaturesPanel" />
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="title">{{ $t('settings.version.title') }}</div>
|
||||
<div class="title">
|
||||
{{ $t('settings.version.title') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<dl>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { mapState } from 'pinia'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
|
||||
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
|
@ -27,8 +26,15 @@ const AccountActions = {
|
|||
ProgressButton,
|
||||
Popover,
|
||||
UserListMenu,
|
||||
ConfirmModal,
|
||||
UserTimedFilterModal,
|
||||
ConfirmModal: defineAsyncComponent(
|
||||
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||
),
|
||||
UserTimedFilterModal: defineAsyncComponent(
|
||||
() =>
|
||||
import(
|
||||
'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
||||
),
|
||||
),
|
||||
},
|
||||
methods: {
|
||||
showConfirmRemoveUserFromFollowers() {
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@
|
|||
</template>
|
||||
</Popover>
|
||||
<teleport to="#modal">
|
||||
<confirm-modal
|
||||
<ConfirmModal
|
||||
v-if="showingConfirmBlock && !blockExpiration"
|
||||
ref="blockDialog"
|
||||
:title="$t('user_card.block_confirm_title')"
|
||||
|
|
@ -114,10 +114,10 @@
|
|||
/>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
</ConfirmModal>
|
||||
</teleport>
|
||||
<teleport to="#modal">
|
||||
<confirm-modal
|
||||
<ConfirmModal
|
||||
v-if="showingConfirmRemoveFollower"
|
||||
:title="$t('user_card.remove_follower_confirm_title')"
|
||||
:confirm-text="$t('user_card.remove_follower_confirm_accept_button')"
|
||||
|
|
@ -136,7 +136,7 @@
|
|||
/>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
</ConfirmModal>
|
||||
<UserTimedFilterModal
|
||||
v-if="blockExpiration"
|
||||
ref="timedBlockDialog"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export default {
|
|||
validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'],
|
||||
variants: {
|
||||
normal: '.neutral',
|
||||
info: '.info',
|
||||
error: '.error',
|
||||
warning: '.warning',
|
||||
success: '.success',
|
||||
|
|
@ -47,5 +48,11 @@ export default {
|
|||
background: '--cGreen',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'info',
|
||||
directives: {
|
||||
background: '--cBlue',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import AnnouncementEditor from 'src/components/announcement_editor/announcement_editor.vue'
|
||||
import localeService from '../../services/locale/locale.service.js'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
import RichContent from '../rich_content/rich_content.jsx'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements.js'
|
||||
|
||||
const Announcement = {
|
||||
components: {
|
||||
AnnouncementEditor,
|
||||
RichContent,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -33,9 +31,7 @@ const Announcement = {
|
|||
canEditAnnouncement() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.privileges.includes(
|
||||
'announcements_manage_announcements',
|
||||
)
|
||||
this.currentUser.privileges.has('announcements_manage_announcements')
|
||||
)
|
||||
},
|
||||
content() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const AnnouncementEditor = {
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import Announcement from '../announcement/announcement.vue'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
import Announcement from 'src/components/announcement/announcement.vue'
|
||||
import AnnouncementEditor from 'src/components/announcement_editor/announcement_editor.vue'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements.js'
|
||||
|
||||
|
|
@ -35,9 +35,7 @@ const AnnouncementsPage = {
|
|||
canPostAnnouncement() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.privileges.includes(
|
||||
'announcements_manage_announcements',
|
||||
)
|
||||
this.currentUser.privileges.has('announcements_manage_announcements')
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { mapState } from 'pinia'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import nsfwImage from '../../assets/nsfw.png'
|
||||
import Flash from '../flash/flash.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
|
|
@ -53,6 +52,7 @@ const Attachment = {
|
|||
'shiftDn',
|
||||
'edit',
|
||||
],
|
||||
emits: ['play', 'pause', 'naturalSizeLoad'],
|
||||
data() {
|
||||
return {
|
||||
localDescription: this.description || this.attachment.description,
|
||||
|
|
@ -65,13 +65,15 @@ const Attachment = {
|
|||
modalOpen: false,
|
||||
showHidden: false,
|
||||
flashLoaded: false,
|
||||
showDescription: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Flash,
|
||||
StillImage,
|
||||
VideoAttachment,
|
||||
Flash: defineAsyncComponent(() => import('src/components/flash/flash.vue')),
|
||||
|
||||
VideoAttachment: defineAsyncComponent(
|
||||
() => import('src/components/video_attachment/video_attachment.vue'),
|
||||
),
|
||||
Popover,
|
||||
},
|
||||
computed: {
|
||||
classNames() {
|
||||
|
|
@ -180,9 +182,6 @@ const Attachment = {
|
|||
setFlashLoaded(event) {
|
||||
this.flashLoaded = event
|
||||
},
|
||||
toggleDescription() {
|
||||
this.showDescription = !this.showDescription
|
||||
},
|
||||
toggleHidden(event) {
|
||||
if (
|
||||
this.mergedConfig.useOneClickNsfw &&
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@
|
|||
width: 2em;
|
||||
height: 2em;
|
||||
margin-left: 0.5em;
|
||||
font-size: 1.25em;
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -265,3 +265,27 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.description-popover {
|
||||
padding: 1em;
|
||||
width: 50ch;
|
||||
max-width: 90vw;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
summary {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: bold;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
max-height: 10.5em;
|
||||
text-wrap: pretty;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,21 +30,16 @@
|
|||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)"
|
||||
v-if="size !== 'hide' && !hideDescription && edit"
|
||||
class="description-container"
|
||||
:class="{ '-static': !edit }"
|
||||
>
|
||||
<input
|
||||
v-if="edit"
|
||||
<textarea
|
||||
v-model="localDescription"
|
||||
type="text"
|
||||
class="input description-field"
|
||||
:placeholder="$t('post_status.media_description')"
|
||||
@keydown.enter.prevent=""
|
||||
>
|
||||
<p v-else>
|
||||
{{ localDescription }}
|
||||
</p>
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
|
|
@ -87,14 +82,22 @@
|
|||
>
|
||||
<FAIcon icon="stop" />
|
||||
</button>
|
||||
<button
|
||||
<Popover
|
||||
v-if="attachment.description && size !== 'small' && !edit && attachment.type !== 'unknown'"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.show_attachment_description')"
|
||||
@click.prevent="toggleDescription"
|
||||
trigger="click"
|
||||
popover-class="popover popover-default description-popover"
|
||||
:trigger-attrs="{ 'class': 'button-default attachment-button -transparent', 'title': $t('status.attachment_description') }"
|
||||
>
|
||||
<FAIcon icon="align-right" />
|
||||
</button>
|
||||
<template #trigger>
|
||||
<FAIcon icon="align-right" />
|
||||
</template>
|
||||
<template #content>
|
||||
<details open>
|
||||
<summary>{{ $t('status.attachment_description') }}</summary>
|
||||
<span>{{ localDescription }}</span>
|
||||
</details>
|
||||
</template>
|
||||
</Popover>
|
||||
<button
|
||||
v-if="!useModal && attachment.type !== 'unknown'"
|
||||
class="button-default attachment-button -transparent"
|
||||
|
|
@ -244,21 +247,16 @@
|
|||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))"
|
||||
v-if="size !== 'hide' && !hideDescription && edit"
|
||||
class="description-container"
|
||||
:class="{ '-static': !edit }"
|
||||
>
|
||||
<input
|
||||
v-if="edit"
|
||||
<textarea
|
||||
v-model="localDescription"
|
||||
type="text"
|
||||
class="input description-field"
|
||||
:placeholder="$t('post_status.media_description')"
|
||||
@keydown.enter.prevent=""
|
||||
>
|
||||
<p v-else>
|
||||
{{ localDescription }}
|
||||
</p>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { mapState } from 'pinia'
|
||||
import { h, resolveComponent } from 'vue'
|
||||
|
||||
import LoginForm from '../login_form/login_form.vue'
|
||||
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
|
||||
import MFATOTPForm from '../mfa_form/totp_form.vue'
|
||||
import LoginForm from 'src/components/login_form/login_form.vue'
|
||||
import MFARecoveryForm from 'src/components/mfa_form/recovery_form.vue'
|
||||
import MFATOTPForm from 'src/components/mfa_form/totp_form.vue'
|
||||
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserLink from '../user_link/user_link.vue'
|
||||
import UserPopover from '../user_popover/user_popover.vue'
|
||||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||
import UserLink from 'src/components/user_link/user_link.vue'
|
||||
import UserPopover from 'src/components/user_popover/user_popover.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
|
@ -9,11 +8,19 @@ import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
|||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
const BasicUserCard = {
|
||||
props: ['user'],
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
},
|
||||
showLineLabels: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
UserPopover,
|
||||
UserAvatar,
|
||||
RichContent,
|
||||
|
||||
UserLink,
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -29,7 +36,7 @@ const BasicUserCard = {
|
|||
allowNonSquareEmoji() {
|
||||
return useMergedConfigStore().mergedConfig.nonSquareEmoji
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default BasicUserCard
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@
|
|||
:title="user.name"
|
||||
class="basic-user-card-user-name"
|
||||
>
|
||||
<strong v-if="showLineLabels">
|
||||
{{ $t('admin_dash.users.labels.name_colon') }}
|
||||
{{ ' ' }}
|
||||
</strong>
|
||||
<RichContent
|
||||
class="basic-user-card-user-name-value"
|
||||
:html="user.name"
|
||||
|
|
@ -31,6 +35,10 @@
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<strong v-if="showLineLabels">
|
||||
{{ $t('admin_dash.users.labels.handle_colon') }}
|
||||
{{ ' ' }}
|
||||
</strong>
|
||||
<user-link
|
||||
class="basic-user-card-screen-name"
|
||||
:user="user"
|
||||
|
|
@ -46,8 +54,10 @@
|
|||
<style lang="scss">
|
||||
.basic-user-card {
|
||||
display: flex;
|
||||
flex: 1 0;
|
||||
flex: 1 1 10em;
|
||||
min-width: 1em;
|
||||
margin: 0;
|
||||
line-height: 1.25;
|
||||
|
||||
--emoji-size: 1em;
|
||||
|
||||
|
|
@ -69,7 +79,7 @@
|
|||
|
||||
&-user-name-value,
|
||||
&-screen-name {
|
||||
display: inline-block;
|
||||
display: inline;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mapState } from 'pinia'
|
||||
|
||||
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
|
||||
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<basic-user-card :user="user">
|
||||
<BasicUserCard :user="user">
|
||||
<div class="block-card-content-container">
|
||||
<span
|
||||
v-if="blocked && blockExpiryAvailable"
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
:is-mute="false"
|
||||
/>
|
||||
</teleport>
|
||||
</basic-user-card>
|
||||
</BasicUserCard>
|
||||
</template>
|
||||
|
||||
<script src="./block_card.js"></script>
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faEllipsisH)
|
||||
|
||||
const BookmarkFolderCard = {
|
||||
props: ['folder', 'allBookmarks'],
|
||||
computed: {
|
||||
firstLetter() {
|
||||
return this.folder ? this.folder.name[0] : null
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default BookmarkFolderCard
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="allBookmarks"
|
||||
class="bookmark-folder-card"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'bookmarks' }"
|
||||
class="bookmark-folder-name"
|
||||
>
|
||||
<span class="icon">
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 menu-icon"
|
||||
icon="bookmark"
|
||||
/>
|
||||
</span>{{ $t('nav.all_bookmarks') }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="bookmark-folder-card"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: 'bookmark-folder', params: { id: folder.id } }"
|
||||
class="bookmark-folder-name"
|
||||
>
|
||||
<img
|
||||
v-if="folder.emoji_url"
|
||||
class="iconEmoji iconEmoji-image"
|
||||
:src="folder.emoji_url"
|
||||
:alt="folder.emoji"
|
||||
:title="folder.emoji"
|
||||
>
|
||||
<span
|
||||
v-else-if="folder.emoji"
|
||||
class="iconEmoji"
|
||||
>
|
||||
<span>
|
||||
{{ folder.emoji }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-else-if="firstLetter"
|
||||
class="icon iconLetter fa-scale-110"
|
||||
>{{ firstLetter }}</span>{{ folder.name }}
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'bookmark-folder-edit', params: { id: folder.id } }"
|
||||
class="button-folder-edit"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="ellipsis-h"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./bookmark_folder_card.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.bookmark-folder-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
a.bookmark-folder-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
|
||||
.icon,
|
||||
.iconLetter,
|
||||
.iconEmoji {
|
||||
display: inline-block;
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.icon,
|
||||
.iconLetter {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.iconEmoji {
|
||||
text-align: center;
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
|
||||
> span {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
img.iconEmoji {
|
||||
padding: 0.25em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.bookmark-folder-name,
|
||||
.button-folder-edit {
|
||||
margin: 0;
|
||||
padding: 1em;
|
||||
color: var(--link);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import apiService from '../../services/api/api.service'
|
||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||
import EmojiPicker from 'src/components/emoji_picker/emoji_picker.vue'
|
||||
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
|
||||
import { fetchBookmarkFolders } from 'src/api/user.js'
|
||||
|
||||
const BookmarkFolderEdit = {
|
||||
data() {
|
||||
|
|
@ -22,8 +24,10 @@ const BookmarkFolderEdit = {
|
|||
},
|
||||
created() {
|
||||
if (!this.id) return
|
||||
const credentials = this.$store.state.users.currentUser.credentials
|
||||
apiService.fetchBookmarkFolders({ credentials }).then((folders) => {
|
||||
|
||||
fetchBookmarkFolders({
|
||||
credentials: useOAuthStore().token,
|
||||
}).then(({ data: folders }) => {
|
||||
const folder = folders.find((folder) => folder.id === this.id)
|
||||
if (!folder) return
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue'
|
||||
import FolderCard from 'src/components/folder_card/folder_card.vue'
|
||||
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ const BookmarkFolders = {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
BookmarkFolderCard,
|
||||
FolderCard,
|
||||
},
|
||||
computed: {
|
||||
bookmarkFolders() {
|
||||
|
|
|
|||
|
|
@ -12,14 +12,28 @@
|
|||
</router-link>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<BookmarkFolderCard
|
||||
:all-bookmarks="true"
|
||||
class="list-item"
|
||||
/>
|
||||
<BookmarkFolderCard
|
||||
<div class="list-item FolderCard">
|
||||
<router-link
|
||||
:to="{ name: 'bookmarks' }"
|
||||
class="folder-name"
|
||||
>
|
||||
<span class="icon">
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 menu-icon"
|
||||
icon="bookmark"
|
||||
/>
|
||||
</span>{{ $t('nav.all_bookmarks') }}
|
||||
</router-link>
|
||||
</div>
|
||||
<FolderCard
|
||||
v-for="folder in bookmarkFolders.slice().reverse()"
|
||||
:key="folder"
|
||||
:folder="folder"
|
||||
:name="folder.name"
|
||||
:emoji="folder.emoji"
|
||||
:emoji-url="folder.emoji_url"
|
||||
:link="{ name: 'bookmark-folder', params: { id: folder.id } }"
|
||||
:link-edit="{ name: 'bookmark-folder-edit', params: { id: folder.id } }"
|
||||
class="list-item"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
import Timeline from 'src/components/timeline/timeline.vue'
|
||||
|
||||
const Bookmarks = {
|
||||
created() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
import Timeline from 'src/components/timeline/timeline.vue'
|
||||
|
||||
const BubbleTimeline = {
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import _ from 'lodash'
|
||||
import { throttle } from 'lodash'
|
||||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
import { WSConnectionStatus } from '../../services/api/api.service.js'
|
||||
import ChatMessage from 'src/components/chat_message/chat_message.vue'
|
||||
import ChatTitle from 'src/components/chat_title/chat_title.vue'
|
||||
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
|
||||
import chatService from '../../services/chat_service/chat_service.js'
|
||||
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
|
||||
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
|
||||
import ChatMessage from '../chat_message/chat_message.vue'
|
||||
import ChatTitle from '../chat_title/chat_title.vue'
|
||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import {
|
||||
getNewTopPosition,
|
||||
getScrollPosition,
|
||||
|
|
@ -17,6 +16,14 @@ import {
|
|||
} from './chat_layout_utils.js'
|
||||
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
|
||||
import {
|
||||
chatMessages,
|
||||
getOrCreateChat,
|
||||
sendChatMessage,
|
||||
} from 'src/api/chats.js'
|
||||
import { WSConnectionStatus } from 'src/api/websocket.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
|
||||
|
|
@ -115,7 +122,6 @@ const Chat = {
|
|||
mobileLayout: (store) => store.layoutType === 'mobile',
|
||||
}),
|
||||
...mapState({
|
||||
backendInteractor: (state) => state.api.backendInteractor,
|
||||
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
}),
|
||||
|
|
@ -224,7 +230,7 @@ const Chat = {
|
|||
}
|
||||
}, 5000)
|
||||
},
|
||||
handleScroll: _.throttle(function () {
|
||||
handleScroll: throttle(function () {
|
||||
this.lastScrollPosition = getScrollPosition()
|
||||
if (!this.currentChat) {
|
||||
return
|
||||
|
|
@ -267,43 +273,48 @@ const Chat = {
|
|||
const fetchOlderMessages = !!maxId
|
||||
const sinceId = fetchLatest && chatMessageService.maxId
|
||||
|
||||
return this.backendInteractor
|
||||
.chatMessages({ id: chatId, maxId, sinceId })
|
||||
.then((messages) => {
|
||||
// Clear the current chat in case we're recovering from a ws connection loss.
|
||||
if (isFirstFetch) {
|
||||
chatService.clear(chatMessageService)
|
||||
}
|
||||
return chatMessages({
|
||||
id: chatId,
|
||||
maxId,
|
||||
sinceId,
|
||||
credentials: useOAuthStore().token,
|
||||
}).then(({ data: messages }) => {
|
||||
// Clear the current chat in case we're recovering from a ws connection loss.
|
||||
if (isFirstFetch) {
|
||||
chatService.clear(chatMessageService)
|
||||
}
|
||||
|
||||
const positionBeforeUpdate = getScrollPosition()
|
||||
this.$store
|
||||
.dispatch('addChatMessages', { chatId, messages })
|
||||
.then(() => {
|
||||
this.$nextTick(() => {
|
||||
if (fetchOlderMessages) {
|
||||
this.handleScrollUp(positionBeforeUpdate)
|
||||
}
|
||||
const positionBeforeUpdate = getScrollPosition()
|
||||
this.$store
|
||||
.dispatch('addChatMessages', { chatId, messages })
|
||||
.then(() => {
|
||||
this.$nextTick(() => {
|
||||
if (fetchOlderMessages) {
|
||||
this.handleScrollUp(positionBeforeUpdate)
|
||||
}
|
||||
|
||||
// In vertical screens, the first batch of fetched messages may not always take the
|
||||
// full height of the scrollable container.
|
||||
// If this is the case, we want to fetch the messages until the scrollable container
|
||||
// is fully populated so that the user has the ability to scroll up and load the history.
|
||||
if (!isScrollable() && messages.length > 0) {
|
||||
this.fetchChat({
|
||||
maxId: this.currentChatMessageService.minId,
|
||||
})
|
||||
}
|
||||
})
|
||||
// In vertical screens, the first batch of fetched messages may not always take the
|
||||
// full height of the scrollable container.
|
||||
// If this is the case, we want to fetch the messages until the scrollable container
|
||||
// is fully populated so that the user has the ability to scroll up and load the history.
|
||||
if (!isScrollable() && messages.length > 0) {
|
||||
this.fetchChat({
|
||||
maxId: this.currentChatMessageService.minId,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
async startFetching() {
|
||||
let chat = this.findOpenedChatByRecipientId(this.recipientId)
|
||||
if (!chat) {
|
||||
try {
|
||||
chat = await this.backendInteractor.getOrCreateChat({
|
||||
const { data } = await getOrCreateChat({
|
||||
accountId: this.recipientId,
|
||||
credentials: useOAuthStore().token,
|
||||
})
|
||||
chat = data
|
||||
} catch (e) {
|
||||
console.error('Error creating or getting a chat', e)
|
||||
this.errorLoadingChat = true
|
||||
|
|
@ -369,9 +380,11 @@ const Chat = {
|
|||
doSendMessage({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
|
||||
if (retriesLeft <= 0) return
|
||||
|
||||
this.backendInteractor
|
||||
.sendChatMessage(params)
|
||||
.then((data) => {
|
||||
sendChatMessage({
|
||||
params,
|
||||
credentials: useOAuthStore().token,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
this.$store.dispatch('addChatMessages', {
|
||||
chatId: this.currentChat.id,
|
||||
updateMaxId: false,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
import ChatListItem from '../chat_list_item/chat_list_item.vue'
|
||||
import ChatNew from '../chat_new/chat_new.vue'
|
||||
import List from '../list/list.vue'
|
||||
import ChatListItem from 'src/components/chat_list_item/chat_list_item.vue'
|
||||
import ChatNew from 'src/components/chat_new/chat_new.vue'
|
||||
import List from 'src/components/list/list.vue'
|
||||
|
||||
const ChatList = {
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
v-if="sortedChatList.length > 0"
|
||||
class="timeline"
|
||||
>
|
||||
<List :items="sortedChatList">
|
||||
<List :external-items="sortedChatList">
|
||||
<template #item="{item}">
|
||||
<ChatListItem
|
||||
:key="item.id"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import AvatarList from '../avatar_list/avatar_list.vue'
|
||||
import ChatTitle from '../chat_title/chat_title.vue'
|
||||
import StatusBody from '../status_content/status_content.vue'
|
||||
import Timeago from '../timeago/timeago.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import AvatarList from 'src/components/avatar_list/avatar_list.vue'
|
||||
import ChatTitle from 'src/components/chat_title/chat_title.vue'
|
||||
import StatusBody from 'src/components/status_content/status_content.vue'
|
||||
import Timeago from 'src/components/timeago/timeago.vue'
|
||||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||
|
||||
const ChatListItem = {
|
||||
name: 'ChatListItem',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import Attachment from '../attachment/attachment.vue'
|
||||
import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
|
||||
import Gallery from '../gallery/gallery.vue'
|
||||
import LinkPreview from '../link-preview/link-preview.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import StatusContent from '../status_content/status_content.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import Attachment from 'src/components/attachment/attachment.vue'
|
||||
import ChatMessageDate from 'src/components/chat_message_date/chat_message_date.vue'
|
||||
import Gallery from 'src/components/gallery/gallery.vue'
|
||||
import LinkPreview from 'src/components/link-preview/link-preview.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import StatusContent from 'src/components/status_content/status_content.vue'
|
||||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||
import UserPopover from 'src/components/user_popover/user_popover.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
|
@ -37,9 +37,7 @@ const ChatMessage = {
|
|||
Gallery,
|
||||
LinkPreview,
|
||||
ChatMessageDate,
|
||||
UserPopover: defineAsyncComponent(
|
||||
() => import('../user_popover/user_popover.vue'),
|
||||
),
|
||||
UserPopover,
|
||||
},
|
||||
computed: {
|
||||
// Returns HH:MM (hours and minutes) in local time.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
|
||||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||
|
|
@ -22,7 +24,9 @@ const chatNew = {
|
|||
}
|
||||
},
|
||||
async created() {
|
||||
const { chats } = await this.backendInteractor.chats()
|
||||
const { chats } = await chats({
|
||||
credentials: useOAuthStore().token,
|
||||
})
|
||||
chats.forEach((chat) => this.suggestions.push(chat.account))
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -38,7 +42,6 @@ const chatNew = {
|
|||
},
|
||||
...mapState({
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
backendInteractor: (state) => state.api.backendInteractor,
|
||||
}),
|
||||
...mapGetters(['findUser']),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import { defineAsyncComponent } from 'vue'
|
||||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||
import UserPopover from 'src/components/user_popover/user_popover.vue'
|
||||
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
||||
export default {
|
||||
name: 'ChatTitle',
|
||||
components: {
|
||||
UserAvatar,
|
||||
RichContent,
|
||||
UserPopover: defineAsyncComponent(
|
||||
() => import('../user_popover/user_popover.vue'),
|
||||
),
|
||||
|
||||
UserPopover,
|
||||
},
|
||||
props: ['user', 'withAvatar'],
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -66,8 +66,8 @@
|
|||
<script>
|
||||
import { throttle } from 'lodash'
|
||||
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faEyeDropper } from '@fortawesome/free-solid-svg-icons'
|
||||
|
|
@ -88,6 +88,7 @@ export default {
|
|||
label: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// use unstyled, uh, style
|
||||
unstyled: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import DialogModal from '../dialog_modal/dialog_modal.vue'
|
||||
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faCircleQuestion } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faCircleQuestion)
|
||||
|
||||
/**
|
||||
* This component emits the following events:
|
||||
|
|
|
|||
|
|
@ -8,8 +8,20 @@
|
|||
<span v-text="title" />
|
||||
</template>
|
||||
|
||||
<slot />
|
||||
|
||||
<div class="content">
|
||||
<FAIcon
|
||||
class="confirm-icon"
|
||||
icon="circle-question"
|
||||
size="3x"
|
||||
fixed-width
|
||||
/>
|
||||
<div class="text">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<div class="below">
|
||||
<slot name="below" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<slot name="footerLeft" />
|
||||
<button
|
||||
|
|
@ -29,3 +41,43 @@
|
|||
</template>
|
||||
|
||||
<script src="./confirm_modal.js"></script>
|
||||
<style lang="scss">
|
||||
.confirm-modal {
|
||||
.confirm-icon {
|
||||
margin-left: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
justify-content: center;
|
||||
line-height: 1.5;
|
||||
|
||||
p {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.below:not(:empty) {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.text {
|
||||
max-width: 50ch;
|
||||
margin-left: 0.5em;
|
||||
margin-right: 3.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mapState } from 'pinia'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import ConfirmModal from './confirm_modal.vue'
|
||||
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
||||
|
|
@ -12,7 +12,10 @@ export default {
|
|||
showing: false,
|
||||
}),
|
||||
components: {
|
||||
ConfirmModal,
|
||||
ConfirmModal: defineAsyncComponent(
|
||||
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||
),
|
||||
|
||||
Select,
|
||||
},
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<confirm-modal
|
||||
<ConfirmModal
|
||||
v-if="showing"
|
||||
:title="$t('user_card.mute_confirm_title')"
|
||||
:confirm-text="$t('user_card.mute_confirm_accept_button')"
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
<span v-text="user.screen_name_ui" />
|
||||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
</ConfirmModal>
|
||||
</template>
|
||||
|
||||
<script src="./mute_confirm.js" />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Conversation from '../conversation/conversation.vue'
|
||||
import Conversation from 'src/components/conversation/conversation.vue'
|
||||
|
||||
const conversationPage = {
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
<template>
|
||||
<conversation
|
||||
:collapsable="false"
|
||||
is-page="true"
|
||||
<Conversation
|
||||
:status-id="statusId"
|
||||
is-page
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,16 @@ import { clone, filter, findIndex, get, reduce } from 'lodash'
|
|||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import { WSConnectionStatus } from '../../services/api/api.service.js'
|
||||
import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue'
|
||||
import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue'
|
||||
import Status from '../status/status.vue'
|
||||
import ThreadTree from '../thread_tree/thread_tree.vue'
|
||||
import QuickFilterSettings from 'src/components/quick_filter_settings/quick_filter_settings.vue'
|
||||
import QuickViewSettings from 'src/components/quick_view_settings/quick_view_settings.vue'
|
||||
import ThreadTree from 'src/components/thread_tree/thread_tree.vue'
|
||||
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
|
||||
import { fetchConversation, fetchStatus } from 'src/api/public.js'
|
||||
import { WSConnectionStatus } from 'src/api/websocket.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
|
@ -53,25 +55,56 @@ const sortAndFilterConversation = (conversation, statusoid) => {
|
|||
}
|
||||
|
||||
const conversation = {
|
||||
props: {
|
||||
statusId: {
|
||||
// Main thing
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
collapsable: {
|
||||
// Whether conversation can be collapsed
|
||||
// i.e. when it's not a page
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isPage: {
|
||||
// Whether conversation is rendered as a standalone page
|
||||
// as opposed to embedded into a timeline
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
pinnedStatusIdsObject: {
|
||||
// Used for user profile, map of pinned statuses
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
inProfile: {
|
||||
// Whether conversation is rendered in a user profile
|
||||
// used for overriding muted status
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
profileUserId: {
|
||||
// used with inProfile, user id of the profile
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
virtualHidden: {
|
||||
// Whether conversation is suspended. Controls rendering of statuses
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
highlight: null,
|
||||
focused: null,
|
||||
expanded: false,
|
||||
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
|
||||
statusContentPropertiesObject: {},
|
||||
inlineDivePosition: null,
|
||||
loadStatusError: null,
|
||||
unsuspendibleIds: new Set(),
|
||||
}
|
||||
},
|
||||
props: [
|
||||
'statusId',
|
||||
'collapsable',
|
||||
'isPage',
|
||||
'pinnedStatusIdsObject',
|
||||
'inProfile',
|
||||
'profileUserId',
|
||||
'virtualHidden',
|
||||
],
|
||||
created() {
|
||||
if (this.isPage) {
|
||||
this.fetchConversation()
|
||||
|
|
@ -116,16 +149,7 @@ const conversation = {
|
|||
return this.otherRepliesButtonPosition === 'inside'
|
||||
},
|
||||
suspendable() {
|
||||
if (this.isTreeView) {
|
||||
return Object.entries(this.statusContentProperties).every(
|
||||
([, prop]) => !prop.replying && prop.mediaPlaying.length === 0,
|
||||
)
|
||||
}
|
||||
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
|
||||
return this.$refs.statusComponent.every((s) => s.suspendable)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
return this.unsuspendibleIds.size > 0
|
||||
},
|
||||
hideStatus() {
|
||||
return this.virtualHidden && this.suspendable
|
||||
|
|
@ -362,36 +386,11 @@ const conversation = {
|
|||
return a
|
||||
}, {})
|
||||
},
|
||||
statusContentProperties() {
|
||||
return this.conversation.reduce((a, k) => {
|
||||
const id = k.id
|
||||
const props = (() => {
|
||||
const def = {
|
||||
showingTall: false,
|
||||
expandingSubject: false,
|
||||
showingLongSubject: false,
|
||||
isReplying: false,
|
||||
mediaPlaying: [],
|
||||
}
|
||||
|
||||
if (this.statusContentPropertiesObject[id]) {
|
||||
return {
|
||||
...def,
|
||||
...this.statusContentPropertiesObject[id],
|
||||
}
|
||||
}
|
||||
return def
|
||||
})()
|
||||
|
||||
a[id] = props
|
||||
return a
|
||||
}, {})
|
||||
},
|
||||
canDive() {
|
||||
return this.isTreeView && this.isExpanded
|
||||
},
|
||||
maybeHighlight() {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
maybeFocused() {
|
||||
return this.isExpanded ? this.focused : null
|
||||
},
|
||||
...mapPiniaState(useMergedConfigStore, ['mergedConfig']),
|
||||
...mapState({
|
||||
|
|
@ -402,7 +401,6 @@ const conversation = {
|
|||
}),
|
||||
},
|
||||
components: {
|
||||
Status,
|
||||
ThreadTree,
|
||||
QuickFilterSettings,
|
||||
QuickViewSettings,
|
||||
|
|
@ -416,7 +414,7 @@ const conversation = {
|
|||
oldConversationId &&
|
||||
newConversationId === oldConversationId
|
||||
) {
|
||||
this.setHighlight(this.originalStatusId)
|
||||
this.setFocused(this.originalStatusId)
|
||||
} else {
|
||||
this.fetchConversation()
|
||||
}
|
||||
|
|
@ -438,38 +436,36 @@ const conversation = {
|
|||
methods: {
|
||||
fetchConversation() {
|
||||
if (this.status) {
|
||||
this.$store.state.api.backendInteractor
|
||||
.fetchConversation({ id: this.statusId })
|
||||
.then(({ ancestors, descendants }) => {
|
||||
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
||||
this.$store.dispatch('addNewStatuses', { statuses: descendants })
|
||||
this.setHighlight(this.originalStatusId)
|
||||
})
|
||||
fetchConversation({
|
||||
id: this.statusId,
|
||||
credentials: useOAuthStore().token,
|
||||
}).then(({ data: { ancestors, descendants } }) => {
|
||||
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
||||
this.$store.dispatch('addNewStatuses', { statuses: descendants })
|
||||
this.setFocused(this.originalStatusId)
|
||||
})
|
||||
} else {
|
||||
this.loadStatusError = null
|
||||
this.$store.state.api.backendInteractor
|
||||
.fetchStatus({ id: this.statusId })
|
||||
.then((status) => {
|
||||
fetchStatus({
|
||||
id: this.statusId,
|
||||
credentials: useOAuthStore().token,
|
||||
})
|
||||
.then(({ data: status }) => {
|
||||
this.$store.dispatch('addNewStatuses', { statuses: [status] })
|
||||
this.fetchConversation()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
this.loadStatusError = error
|
||||
})
|
||||
}
|
||||
},
|
||||
isFocused(id) {
|
||||
return this.isExpanded && id === this.highlight
|
||||
},
|
||||
getReplies(id) {
|
||||
return this.replies[id] || []
|
||||
},
|
||||
getHighlight() {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
},
|
||||
setHighlight(id) {
|
||||
setFocused(id) {
|
||||
if (!id) return
|
||||
this.highlight = id
|
||||
this.focused = id
|
||||
|
||||
if (!this.streamingEnabled) {
|
||||
this.$store.dispatch('fetchStatus', id)
|
||||
|
|
@ -509,22 +505,6 @@ const conversation = {
|
|||
showThreadRecursively(id) {
|
||||
this.setThreadDisplayRecursively(id, 'showing')
|
||||
},
|
||||
setStatusContentProperty(id, name, value) {
|
||||
this.statusContentPropertiesObject = {
|
||||
...this.statusContentPropertiesObject,
|
||||
[id]: {
|
||||
...this.statusContentPropertiesObject[id],
|
||||
[name]: value,
|
||||
},
|
||||
}
|
||||
},
|
||||
toggleStatusContentProperty(id, name) {
|
||||
this.setStatusContentProperty(
|
||||
id,
|
||||
name,
|
||||
!this.statusContentProperties[id][name],
|
||||
)
|
||||
},
|
||||
leastVisibleAncestor(id) {
|
||||
let cur = id
|
||||
let parent = this.parentOf(cur)
|
||||
|
|
@ -550,7 +530,7 @@ const conversation = {
|
|||
// only used when we are not on a page
|
||||
undive() {
|
||||
this.inlineDivePosition = null
|
||||
this.setHighlight(this.statusId)
|
||||
this.setFocused(this.statusId)
|
||||
},
|
||||
tryScrollTo(id) {
|
||||
if (!id) {
|
||||
|
|
@ -568,7 +548,7 @@ const conversation = {
|
|||
// contain scrolling calls, as we do not want the page to jump
|
||||
// when we scroll with an expanded conversation.
|
||||
//
|
||||
// Now the method is to rely solely on the `highlight` watcher
|
||||
// Now the method is to rely solely on the `focused` watcher
|
||||
// in `status` components.
|
||||
// In linear views, all statuses are rendered at all times, but
|
||||
// in tree views, it is possible that a change in active status
|
||||
|
|
@ -576,9 +556,9 @@ const conversation = {
|
|||
// status becomes an ancestor status, and thus they will be
|
||||
// different).
|
||||
// Here, let the components be rendered first, in order to trigger
|
||||
// the `highlight` watcher.
|
||||
// the `focused` watcher.
|
||||
this.$nextTick(() => {
|
||||
this.setHighlight(id)
|
||||
this.setFocused(id)
|
||||
})
|
||||
},
|
||||
goToCurrent() {
|
||||
|
|
@ -624,6 +604,13 @@ const conversation = {
|
|||
this.undive()
|
||||
this.threadDisplayStatusObject = {}
|
||||
},
|
||||
onStatusSuspendStateChange({ id, suspend }) {
|
||||
if (!suspend) {
|
||||
this.unsuspendibleIds.add(id)
|
||||
} else {
|
||||
this.unsuspendibleIds.delete(id)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,8 +96,7 @@
|
|||
:replies="getReplies(status.id)"
|
||||
|
||||
:expandable="!isExpanded"
|
||||
:focused="isFocused(status.id)"
|
||||
:highlight="getHighlight()"
|
||||
:focused="maybeFocused === status.id"
|
||||
:inline-expanded="collapsable && isExpanded"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:in-profile="inProfile"
|
||||
|
|
@ -105,21 +104,11 @@
|
|||
:profile-user-id="profileUserId"
|
||||
:simple-tree="treeViewIsSimple"
|
||||
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
||||
:dive="() => diveIntoStatus(status.id)"
|
||||
can-dive
|
||||
|
||||
:controlled-showing-tall="statusContentProperties[status.id].showingTall"
|
||||
:controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')"
|
||||
:controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
|
||||
:controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')"
|
||||
:controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject"
|
||||
:controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')"
|
||||
:controlled-replying="statusContentProperties[status.id].replying"
|
||||
:controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')"
|
||||
:controlled-media-playing="statusContentProperties[status.id].mediaPlaying"
|
||||
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
|
||||
|
||||
@goto="setHighlight"
|
||||
@toggle-expanded="toggleExpanded"
|
||||
@goto="setFocused"
|
||||
@dive="() => diveIntoStatus(status.id)"
|
||||
@suspendable-state-change="onStatusSuspendStateChange"
|
||||
/>
|
||||
<div
|
||||
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
||||
|
|
@ -150,7 +139,7 @@
|
|||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<thread-tree
|
||||
<ThreadTree
|
||||
v-for="status in showingTopLevel"
|
||||
:key="status.id"
|
||||
ref="statusComponent"
|
||||
|
|
@ -164,22 +153,20 @@
|
|||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||
:profile-user-id="profileUserId"
|
||||
|
||||
:is-focused-function="isFocused"
|
||||
:get-replies="getReplies"
|
||||
:highlight="maybeHighlight"
|
||||
:set-highlight="setHighlight"
|
||||
:focused="maybeFocused"
|
||||
:toggle-expanded="toggleExpanded"
|
||||
|
||||
:simple="treeViewIsSimple"
|
||||
:toggle-thread-display="toggleThreadDisplay"
|
||||
:thread-display-status="threadDisplayStatus"
|
||||
:show-thread-recursively="showThreadRecursively"
|
||||
:total-reply-count="totalReplyCount"
|
||||
:total-reply-depth="totalReplyDepth"
|
||||
:status-content-properties="statusContentProperties"
|
||||
:set-status-content-property="setStatusContentProperty"
|
||||
:toggle-status-content-property="toggleStatusContentProperty"
|
||||
:dive="canDive ? diveIntoStatus : undefined"
|
||||
:can-dive="canDive"
|
||||
|
||||
@goto="setFocused"
|
||||
@dive="diveIntoStatus"
|
||||
@suspendable-state-change="onStatusSuspendStateChange"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -187,7 +174,7 @@
|
|||
class="thread-body"
|
||||
>
|
||||
<article>
|
||||
<status
|
||||
<Status
|
||||
v-for="status in conversation"
|
||||
:key="status.id"
|
||||
ref="statusComponent"
|
||||
|
|
@ -196,16 +183,16 @@
|
|||
:replies="getReplies(status.id)"
|
||||
|
||||
:expandable="!isExpanded"
|
||||
:focused="isFocused(status.id)"
|
||||
:highlight="getHighlight()"
|
||||
:focused="maybeFocused === status.id || maybeFocused === status.retweeted_status?.id"
|
||||
:inline-expanded="collapsable && isExpanded"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:in-profile="inProfile"
|
||||
:in-conversation="isExpanded"
|
||||
:profile-user-id="profileUserId"
|
||||
|
||||
@goto="setHighlight"
|
||||
@goto="setFocused"
|
||||
@toggle-expanded="toggleExpanded"
|
||||
@suspendable-state-change="onStatusSuspendStateChange"
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import SearchBar from 'components/search_bar/search_bar.vue'
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
|
@ -39,7 +38,9 @@ library.add(
|
|||
export default {
|
||||
components: {
|
||||
SearchBar,
|
||||
ConfirmModal,
|
||||
ConfirmModal: defineAsyncComponent(
|
||||
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||
),
|
||||
},
|
||||
data: () => ({
|
||||
searchBarHidden: true,
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<teleport to="#modal">
|
||||
<confirm-modal
|
||||
<ConfirmModal
|
||||
v-if="showingConfirmLogout"
|
||||
:title="$t('login.logout_confirm_title')"
|
||||
:confirm-danger="true"
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
@cancelled="hideConfirmLogout"
|
||||
>
|
||||
{{ $t('login.logout_confirm') }}
|
||||
</confirm-modal>
|
||||
</ConfirmModal>
|
||||
</teleport>
|
||||
</nav>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
import Timeline from 'src/components/timeline/timeline.vue'
|
||||
|
||||
const DMs = {
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||
|
||||
const DomainMuteCard = {
|
||||
props: ['domain'],
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { cloneDeep } from 'lodash'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
|
||||
import EditStatusForm from 'src/components/edit_status_form/edit_status_form.vue'
|
||||
import Gallery from 'src/components/gallery/gallery.vue'
|
||||
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
|
||||
import StatusContent from 'src/components/status_content/status_content.vue'
|
||||
|
|
@ -16,8 +15,12 @@ library.add(faPollH)
|
|||
const Draft = {
|
||||
components: {
|
||||
PostStatusForm,
|
||||
EditStatusForm,
|
||||
ConfirmModal,
|
||||
EditStatusForm: defineAsyncComponent(
|
||||
() => import('src/components/edit_status_form/edit_status_form.vue'),
|
||||
),
|
||||
ConfirmModal: defineAsyncComponent(
|
||||
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||
),
|
||||
StatusContent,
|
||||
Gallery,
|
||||
},
|
||||
|
|
@ -66,15 +69,6 @@ const Draft = {
|
|||
localCollapseSubjectDefault() {
|
||||
return useMergedConfigStore().mergedConfig.collapseMessageWithSubject
|
||||
},
|
||||
nsfwClickthrough() {
|
||||
if (!this.draft.nsfw) {
|
||||
return false
|
||||
}
|
||||
if (this.draft.summary && this.localCollapseSubjectDefault) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
editing(newVal) {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
class="faint"
|
||||
>{{ $t('drafts.empty') }}</p>
|
||||
</span>
|
||||
<gallery
|
||||
<Gallery
|
||||
v-if="draft.files?.length !== 0"
|
||||
class="attachments media-body"
|
||||
:compact="true"
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
/>
|
||||
</div>
|
||||
<teleport to="#modal">
|
||||
<confirm-modal
|
||||
<ConfirmModal
|
||||
v-if="showingConfirmDialog"
|
||||
:title="$t('drafts.abandon_confirm_title')"
|
||||
:confirm-text="$t('drafts.abandon_confirm_accept_button')"
|
||||
|
|
@ -86,7 +86,7 @@
|
|||
@cancelled="hideConfirmDialog"
|
||||
>
|
||||
{{ $t('drafts.abandon_confirm') }}
|
||||
</confirm-modal>
|
||||
</ConfirmModal>
|
||||
</teleport>
|
||||
<div class="actions">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
||||
|
|
@ -9,7 +9,9 @@ const DraftCloser = {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
DialogModal,
|
||||
DialogModal: defineAsyncComponent(
|
||||
() => import('src/components/dialog_modal/dialog_modal.vue'),
|
||||
),
|
||||
},
|
||||
emits: ['save', 'discard'],
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<teleport to="#modal">
|
||||
<dialog-modal
|
||||
<DialogModal
|
||||
v-if="showing"
|
||||
v-body-scroll-lock="true"
|
||||
class="confirm-modal"
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
{{ $t('post_status.close_confirm_continue_composing_button') }}
|
||||
</button>
|
||||
</template>
|
||||
</dialog-modal>
|
||||
</DialogModal>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import Draft from 'src/components/draft/draft.vue'
|
||||
import List from 'src/components/list/list.vue'
|
||||
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
|
||||
|
||||
const Drafts = {
|
||||
components: {
|
||||
Draft,
|
||||
List,
|
||||
ConfirmModal,
|
||||
ConfirmModal: defineAsyncComponent(
|
||||
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||
),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showingConfirmDialog: false
|
||||
showingConfirmDialog: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -23,12 +26,14 @@ const Drafts = {
|
|||
this.showingConfirmDialog = true
|
||||
},
|
||||
doAbandonAll() {
|
||||
this.$store.dispatch('abandonAllDrafts').then(() => this.hideConfirmDialog())
|
||||
this.$store
|
||||
.dispatch('abandonAllDrafts')
|
||||
.then(() => this.hideConfirmDialog())
|
||||
},
|
||||
hideConfirmDialog() {
|
||||
this.showingConfirmDialog = false
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default Drafts
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
</div>
|
||||
<template v-else>
|
||||
<List
|
||||
:items="drafts"
|
||||
:external-items="drafts"
|
||||
:non-interactive="true"
|
||||
>
|
||||
<template #item="{ item: draft }">
|
||||
|
|
@ -58,6 +58,7 @@
|
|||
.Drafts {
|
||||
.draft {
|
||||
margin: 1em 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.remove-all {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
|
||||
import statusPosterService from '../../services/status_poster/status_poster.service.js'
|
||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
|
||||
const EditStatusForm = {
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import get from 'lodash/get'
|
||||
import { get } from 'lodash'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import EditStatusForm from '../edit_status_form/edit_status_form.vue'
|
||||
import Modal from '../modal/modal.vue'
|
||||
import Modal from 'src/components/modal/modal.vue'
|
||||
|
||||
import { useEditStatusStore } from 'src/stores/editStatus.js'
|
||||
|
||||
const EditStatusModal = {
|
||||
components: {
|
||||
EditStatusForm,
|
||||
EditStatusForm: defineAsyncComponent(
|
||||
() => import('src/components/edit_status_form/edit_status_form.vue'),
|
||||
),
|
||||
Modal,
|
||||
},
|
||||
data() {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
:params="params"
|
||||
@posted="doCloseModal"
|
||||
@draft-done="doCloseModal"
|
||||
@can-close="doCloseModal"
|
||||
@close-accepted="doCloseModal"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -333,7 +333,6 @@ const EmojiInput = {
|
|||
if (!this.pickerShown) {
|
||||
this.scrollIntoView()
|
||||
this.$refs.picker.showPicker()
|
||||
this.$refs.picker.startEmojiLoad()
|
||||
} else {
|
||||
this.$refs.picker.hidePicker()
|
||||
}
|
||||
|
|
@ -590,7 +589,7 @@ const EmojiInput = {
|
|||
setCaret({ target: { selectionStart } }) {
|
||||
this.caret = selectionStart
|
||||
this.$nextTick(() => {
|
||||
this.$refs.suggestorPopover.updateStyles()
|
||||
this.$refs.suggestorPopover?.updateStyles()
|
||||
})
|
||||
},
|
||||
autoCompleteItemLabel(suggestion) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div
|
||||
ref="root"
|
||||
class="input emoji-input"
|
||||
:class="{ '-with-picker': !hideEmojiButton, '-textarea': this.input?.tagName === 'TEXTAREA' }"
|
||||
:class="{ '-with-picker': !hideEmojiButton, '-textarea': input?.tagName === 'TEXTAREA' }"
|
||||
>
|
||||
<slot
|
||||
:id="'textbox-' + randomSeed"
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
:title="$t('emoji.add_emoji')"
|
||||
@click.prevent="togglePicker"
|
||||
>
|
||||
<FAIcon :icon="['far', 'smile-beam']" />
|
||||
<FAIcon :icon="['far', 'face-smile-beam']" />
|
||||
</button>
|
||||
<EmojiPicker
|
||||
v-if="enableEmojiPicker"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { useEmojiStore } from 'src/stores/emoji.js'
|
||||
|
||||
/**
|
||||
* suggest - generates a suggestor function to be used by emoji-input
|
||||
* data: object providing source information for specific types of suggestions:
|
||||
|
|
@ -28,14 +26,14 @@ export default (data) => {
|
|||
}
|
||||
|
||||
export const suggestEmoji = (emojis) => (input, nameKeywordLocalizer) => {
|
||||
const noPrefix = input.toLowerCase().substr(1)
|
||||
const noPrefix = input.toLowerCase().substring(1)
|
||||
return emojis
|
||||
.map((emoji) => ({ ...emoji, ...nameKeywordLocalizer(emoji) }))
|
||||
.filter(
|
||||
(emoji) =>
|
||||
emoji.names
|
||||
.concat(emoji.keywords)
|
||||
.filter((kw) => kw.toLowerCase().match(noPrefix)).length,
|
||||
.filter((kw) => kw.toLowerCase().includes(noPrefix)).length,
|
||||
)
|
||||
.map((k) => {
|
||||
let score = 0
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import { chunk, debounce, trim } from 'lodash'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import { ensureFinalFallback } from '../../i18n/languages.js'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
|
||||
import { useEmojiStore } from 'src/stores/emoji.js'
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
|
|
@ -129,6 +127,7 @@ const EmojiPicker = {
|
|||
hideCustomEmojiInPicker: false,
|
||||
// Lazy-load only after the first time `showing` becomes true.
|
||||
contentLoaded: false,
|
||||
popoverShown: false,
|
||||
groupRefs: {},
|
||||
emojiRefs: {},
|
||||
filteredEmojiGroups: [],
|
||||
|
|
@ -138,10 +137,10 @@ const EmojiPicker = {
|
|||
},
|
||||
components: {
|
||||
StickerPicker: defineAsyncComponent(
|
||||
() => import('../sticker_picker/sticker_picker.vue'),
|
||||
() => import('src/components/sticker_picker/sticker_picker.vue'),
|
||||
),
|
||||
Checkbox,
|
||||
StillImage,
|
||||
|
||||
Popover,
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -176,6 +175,13 @@ const EmojiPicker = {
|
|||
const fullEmojiSize = emojiSizeReal + 2 * 0.2 * fontSizeMultiplier * 14
|
||||
this.emojiSize = fullEmojiSize
|
||||
},
|
||||
togglePicker() {
|
||||
if (this.popoverShown) {
|
||||
this.hidePicker()
|
||||
} else {
|
||||
this.showPicker()
|
||||
}
|
||||
},
|
||||
showPicker() {
|
||||
this.$refs.popover.showPopover()
|
||||
this.$nextTick(() => {
|
||||
|
|
@ -194,10 +200,10 @@ const EmojiPicker = {
|
|||
}
|
||||
},
|
||||
onPopoverShown() {
|
||||
this.$emit('show')
|
||||
this.popoverShown = true
|
||||
},
|
||||
onPopoverClosed() {
|
||||
this.$emit('close')
|
||||
this.popoverShown = false
|
||||
},
|
||||
onStickerUploaded(e) {
|
||||
this.$emit('sticker-uploaded', e)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
trigger="click"
|
||||
popover-class="emoji-picker popover-default"
|
||||
:hide-trigger="true"
|
||||
placement="bottom"
|
||||
@show="onPopoverShown"
|
||||
@close="onPopoverClosed"
|
||||
>
|
||||
|
|
@ -48,7 +49,7 @@
|
|||
v-if="group.image"
|
||||
class="emoji-picker-header-image"
|
||||
>
|
||||
<still-image
|
||||
<StillImage
|
||||
:alt="group.text"
|
||||
:src="group.image"
|
||||
/>
|
||||
|
|
@ -130,7 +131,7 @@
|
|||
v-if="!emoji.imageUrl"
|
||||
class="emoji-picker-emoji -unicode"
|
||||
>{{ emoji.replacement }}</span>
|
||||
<still-image
|
||||
<StillImage
|
||||
v-else
|
||||
class="emoji-picker-emoji -custom"
|
||||
loading="lazy"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import StillImage from 'src/components/still-image/still-image.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserListPopover from '../user_list_popover/user_list_popover.vue'
|
||||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||
import UserListPopover from 'src/components/user_list_popover/user_list_popover.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
|
@ -17,7 +16,6 @@ const EmojiReactions = {
|
|||
components: {
|
||||
UserAvatar,
|
||||
UserListPopover,
|
||||
StillImage,
|
||||
},
|
||||
props: ['status'],
|
||||
data: () => ({
|
||||
|
|
|
|||
36
src/components/error_modal/error_modal.js
Normal file
36
src/components/error_modal/error_modal.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faCircleXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faCircleXmark)
|
||||
|
||||
/**
|
||||
* This component emits the following events:
|
||||
* cancelled, emitted when the action should not be performed;
|
||||
* accepted, emitted when the action should be performed;
|
||||
*
|
||||
* The caller should close this dialog after receiving any of the two events.
|
||||
*/
|
||||
const ErrorModal = {
|
||||
components: {
|
||||
DialogModal,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
clearText: {
|
||||
type: String,
|
||||
},
|
||||
recoverText: {
|
||||
type: String,
|
||||
},
|
||||
error: {
|
||||
type: Error,
|
||||
},
|
||||
},
|
||||
emits: ['clear', 'recover'],
|
||||
}
|
||||
|
||||
export default ErrorModal
|
||||
103
src/components/error_modal/error_modal.vue
Normal file
103
src/components/error_modal/error_modal.vue
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<DialogModal
|
||||
v-body-scroll-lock="true"
|
||||
class="error-modal"
|
||||
@cancel="onCancel"
|
||||
>
|
||||
<template #header>
|
||||
<span v-text="title ?? $t('general.generic_error')" />
|
||||
</template>
|
||||
|
||||
<div class="content">
|
||||
<FAIcon
|
||||
class="error-icon"
|
||||
icon="circle-xmark"
|
||||
size="3x"
|
||||
fixed-width
|
||||
/>
|
||||
<div class="text">
|
||||
<slot>
|
||||
<p>
|
||||
<strong><code v-text="error.name" /></strong>
|
||||
{{ ' - ' }}
|
||||
<span v-text="error.message" />
|
||||
</p>
|
||||
|
||||
<details open>
|
||||
<summary>{{ $t('general.generic_error_details') }}</summary>
|
||||
<code
|
||||
class="stack pre"
|
||||
v-text="error.stack"
|
||||
/>
|
||||
</details>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="below">
|
||||
<slot name="below" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<slot name="footerLeft" />
|
||||
|
||||
<button
|
||||
v-if="recoverText"
|
||||
class="btn button-default"
|
||||
@click.prevent="$emit('recover')"
|
||||
v-text="recoverText"
|
||||
/>
|
||||
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click.prevent="$emit('clear')"
|
||||
v-text="clearText ?? $t('general.close')"
|
||||
/>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</template>
|
||||
|
||||
<script src="./error_modal.js"></script>
|
||||
<style lang="scss">
|
||||
.error-modal {
|
||||
.error-icon {
|
||||
margin-left: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
justify-content: center;
|
||||
line-height: 1.5;
|
||||
|
||||
p {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0.75em;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stack {
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.below:not(:empty) {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.text {
|
||||
max-width: 50ch;
|
||||
margin-left: 0.5em;
|
||||
margin-right: 3.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
38
src/components/folder_card/folder_card.js
Normal file
38
src/components/folder_card/folder_card.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faEllipsisH)
|
||||
|
||||
const FolderCard = {
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emoji: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
emojiUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
link: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
linkEdit: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
firstLetter() {
|
||||
return this.name[0]
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default FolderCard
|
||||
84
src/components/folder_card/folder_card.vue
Normal file
84
src/components/folder_card/folder_card.vue
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<div class="FolderCard">
|
||||
<router-link
|
||||
:to="link"
|
||||
class="folder-name"
|
||||
>
|
||||
<img
|
||||
v-if="emojiUrl"
|
||||
class="iconEmoji iconEmoji-image"
|
||||
:src="emojiUrl"
|
||||
:alt="emoji"
|
||||
:title="emoji"
|
||||
>
|
||||
<span
|
||||
v-else-if="emoji"
|
||||
class="iconEmoji"
|
||||
>
|
||||
<span>
|
||||
{{ emoji }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-else-if="firstLetter"
|
||||
class="icon iconLetter fa-scale-110"
|
||||
>{{ firstLetter }}</span>{{ name }}
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="linkEdit"
|
||||
class="button-folder-edit"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="ellipsis-h"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./folder_card.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.FolderCard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.folder-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
|
||||
.icon,
|
||||
.iconLetter,
|
||||
.iconEmoji {
|
||||
display: inline-block;
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.icon,
|
||||
.iconLetter {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.iconEmoji {
|
||||
text-align: center;
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
|
||||
> span {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
img.iconEmoji {
|
||||
padding: 0.25em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,14 +1,17 @@
|
|||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import {
|
||||
requestFollow,
|
||||
requestUnfollow,
|
||||
} from '../../services/follow_manipulate/follow_manipulate'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
export default {
|
||||
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
|
||||
components: {
|
||||
ConfirmModal,
|
||||
ConfirmModal: defineAsyncComponent(
|
||||
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||
),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
>
|
||||
{{ label }}
|
||||
<teleport to="#modal">
|
||||
<confirm-modal
|
||||
<ConfirmModal
|
||||
v-if="showingConfirmUnfollow"
|
||||
:title="$t('user_card.unfollow_confirm_title')"
|
||||
:confirm-text="$t('user_card.unfollow_confirm_accept_button')"
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
/>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
</ConfirmModal>
|
||||
</teleport>
|
||||
</button>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import FollowButton from '../follow_button/follow_button.vue'
|
||||
import RemoteFollow from '../remote_follow/remote_follow.vue'
|
||||
import RemoveFollowerButton from '../remove_follower_button/remove_follower_button.vue'
|
||||
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
|
||||
import FollowButton from 'src/components/follow_button/follow_button.vue'
|
||||
import RemoteFollow from 'src/components/remote_follow/remote_follow.vue'
|
||||
import RemoveFollowerButton from 'src/components/remove_follower_button/remove_follower_button.vue'
|
||||
|
||||
const FollowCard = {
|
||||
props: ['user', 'noFollowsYou'],
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js'
|
||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
|
||||
import { approveUser, denyUser } from 'src/api/user.js'
|
||||
|
||||
const FollowRequestCard = {
|
||||
props: ['user'],
|
||||
components: {
|
||||
BasicUserCard,
|
||||
ConfirmModal,
|
||||
ConfirmModal: defineAsyncComponent(
|
||||
() => import('src/components/confirm_modal/confirm_modal.vue'),
|
||||
),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -45,7 +51,10 @@ const FollowRequestCard = {
|
|||
}
|
||||
},
|
||||
doApprove() {
|
||||
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
|
||||
approveUser({
|
||||
id: this.user.id,
|
||||
credentials: useOAuthStore().token,
|
||||
})
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
|
||||
const notifId = this.findFollowRequestNotificationId()
|
||||
|
|
@ -67,12 +76,14 @@ const FollowRequestCard = {
|
|||
},
|
||||
doDeny() {
|
||||
const notifId = this.findFollowRequestNotificationId()
|
||||
this.$store.state.api.backendInteractor
|
||||
.denyUser({ id: this.user.id })
|
||||
.then(() => {
|
||||
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
})
|
||||
|
||||
denyUser({
|
||||
id: this.user.id,
|
||||
credentials: useOAuthStore().token,
|
||||
}).then(() => {
|
||||
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
})
|
||||
this.hideDenyConfirmDialog()
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
</button>
|
||||
</div>
|
||||
<teleport to="#modal">
|
||||
<confirm-modal
|
||||
<ConfirmModal
|
||||
v-if="showingApproveConfirmDialog"
|
||||
:title="$t('user_card.approve_confirm_title')"
|
||||
:confirm-text="$t('user_card.approve_confirm_accept_button')"
|
||||
|
|
@ -24,8 +24,8 @@
|
|||
@cancelled="hideApproveConfirmDialog"
|
||||
>
|
||||
{{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }}
|
||||
</confirm-modal>
|
||||
<confirm-modal
|
||||
</ConfirmModal>
|
||||
<ConfirmModal
|
||||
v-if="showingDenyConfirmDialog"
|
||||
:title="$t('user_card.deny_confirm_title')"
|
||||
:confirm-text="$t('user_card.deny_confirm_accept_button')"
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
@cancelled="hideDenyConfirmDialog"
|
||||
>
|
||||
{{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }}
|
||||
</confirm-modal>
|
||||
</ConfirmModal>
|
||||
</teleport>
|
||||
</basic-user-card>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import FollowRequestCard from '../follow_request_card/follow_request_card.vue'
|
||||
import FollowRequestCard from 'src/components/follow_request_card/follow_request_card.vue'
|
||||
|
||||
const FollowRequests = {
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import LocalSettingIndicator from 'src/components/settings_modal/helpers/local_setting_indicator.vue'
|
||||
import Select from '../select/select.vue'
|
||||
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
import Timeline from 'src/components/timeline/timeline.vue'
|
||||
|
||||
const FriendsTimeline = {
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { set, sumBy } from 'lodash'
|
||||
|
||||
import Attachment from '../attachment/attachment.vue'
|
||||
import Attachment from 'src/components/attachment/attachment.vue'
|
||||
|
||||
import { useMediaViewerStore } from 'src/stores/media_viewer.js'
|
||||
|
||||
|
|
@ -27,8 +27,10 @@ const Gallery = {
|
|||
return {
|
||||
sizes: {},
|
||||
hidingLong: true,
|
||||
playingMedia: new Set(),
|
||||
}
|
||||
},
|
||||
emits: ['play', 'pause'],
|
||||
components: { Attachment },
|
||||
computed: {
|
||||
rows() {
|
||||
|
|
@ -47,7 +49,7 @@ const Gallery = {
|
|||
: attachments
|
||||
.reduce(
|
||||
(acc, attachment, i) => {
|
||||
const peek = attachments[i+1]
|
||||
const peek = attachments[i + 1]
|
||||
const nextEnd = peek == null
|
||||
const nextWide = !nextEnd && !displayTypes.has(peek?.type)
|
||||
|
||||
|
|
@ -68,12 +70,23 @@ const Gallery = {
|
|||
}
|
||||
|
||||
const maxPerRow = 3
|
||||
const currentRow = acc[acc.length - 1].items
|
||||
if ((nextWide || nextEnd) && currentRow.length >= maxPerRow) {
|
||||
const last = currentRow.splice(-1)[0]
|
||||
return [...acc, { items: [last, attachment] }]
|
||||
const currentRow = acc[acc.length - 1]
|
||||
const previousRow = acc[acc.length - 2]
|
||||
|
||||
if (currentRow.items.length >= maxPerRow) {
|
||||
if (nextWide || nextEnd) {
|
||||
if (previousRow?.items.length > 1) {
|
||||
currentRow.items.push(attachment)
|
||||
return [...acc, { items: [] }]
|
||||
} else {
|
||||
const last = currentRow.items.splice(-1)[0]
|
||||
return [...acc, { items: [last, attachment] }]
|
||||
}
|
||||
} else {
|
||||
return [...acc, { items: [attachment] }]
|
||||
}
|
||||
} else {
|
||||
currentRow.push(attachment)
|
||||
currentRow.items.push(attachment)
|
||||
}
|
||||
return acc
|
||||
},
|
||||
|
|
@ -104,11 +117,21 @@ const Gallery = {
|
|||
return this.attachmentsDimensionalScore > 1
|
||||
}
|
||||
},
|
||||
hasPlayingMedia() {
|
||||
return this.playingMedia.size > 0
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onNaturalSizeLoad({ id, width, height }) {
|
||||
set(this.sizes, id, { width, height })
|
||||
},
|
||||
onMediaStateChange(playing, id) {
|
||||
if (playing) {
|
||||
this.playingMedia.add(id)
|
||||
} else {
|
||||
this.playingMedia.delete(id)
|
||||
}
|
||||
},
|
||||
rowStyle(row) {
|
||||
if (row.audio) {
|
||||
return { 'padding-bottom': '25%' } // fixed reduced height for audio
|
||||
|
|
@ -135,6 +158,15 @@ const Gallery = {
|
|||
useMediaViewerStore().setMedia(this.attachments)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
hasPlayingMedia(newValue) {
|
||||
if (newValue) {
|
||||
this.$emit('play')
|
||||
} else {
|
||||
this.$emit('pause')
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default Gallery
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@
|
|||
:style="itemStyle(attachment.id, row.items)"
|
||||
@set-media="onMedia"
|
||||
@natural-size-load="onNaturalSizeLoad"
|
||||
@play="() => onMediaStateChange(true, attachment.id)"
|
||||
@pause="() => onMediaStateChange(false, attachment.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -129,7 +131,7 @@
|
|||
|
||||
.gallery-item {
|
||||
margin: 0;
|
||||
height: 15em;
|
||||
height: 20em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
56
src/components/global_error/global_error.js
Normal file
56
src/components/global_error/global_error.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { mapActions, mapState } from 'pinia'
|
||||
|
||||
import ErrorModal from 'src/components/error_modal/error_modal.vue'
|
||||
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
|
||||
const GlobalError = {
|
||||
components: {
|
||||
ErrorModal,
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
if (this.globalError == null) return null
|
||||
return this.globalError.title && this.$t(this.globalError.title)
|
||||
},
|
||||
content() {
|
||||
if (this.globalError == null) return null
|
||||
if (this.globalError.content) {
|
||||
return this.$t(this.globalError.content, [this.globalError.error])
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
details() {
|
||||
if (this.globalError == null) return null
|
||||
if (this.globalError.error != null) {
|
||||
return (
|
||||
this.globalError.error.toString() +
|
||||
'\n\n' +
|
||||
this.globalError.error.stack
|
||||
)
|
||||
} else {
|
||||
return this.globalError.details
|
||||
}
|
||||
},
|
||||
recoverText() {
|
||||
if (this.globalError == null) return null
|
||||
if (this.globalError.recoverText == null) return null
|
||||
return this.$t(this.globalError.recoverText)
|
||||
},
|
||||
...mapState(useInterfaceStore, ['globalError']),
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
this.globalError.clear?.()
|
||||
this.clearGlobalError()
|
||||
},
|
||||
recover() {
|
||||
this.globalError.recover?.()
|
||||
this.clearGlobalError()
|
||||
},
|
||||
...mapActions(useInterfaceStore, ['clearGlobalError']),
|
||||
},
|
||||
}
|
||||
|
||||
export default GlobalError
|
||||
26
src/components/global_error/global_error.vue
Normal file
26
src/components/global_error/global_error.vue
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<teleport to="#modal">
|
||||
<ErrorModal
|
||||
v-if="globalError"
|
||||
:error="globalError.error"
|
||||
:title="title"
|
||||
:recover-text="recoverText"
|
||||
@recover="recover"
|
||||
@clear="clear"
|
||||
>
|
||||
<template v-if="content">
|
||||
<p>{{ content }}</p>
|
||||
|
||||
<details v-if="details">
|
||||
<summary>{{ $t('general.generic_error_details') }}</summary>
|
||||
<code
|
||||
class="stack pre"
|
||||
v-text="details"
|
||||
/>
|
||||
</details>
|
||||
</template>
|
||||
</ErrorModal>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script src="./global_error.js"></script>
|
||||
|
|
@ -11,3 +11,9 @@
|
|||
</template>
|
||||
|
||||
<script src="./instance_specific_panel.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.instance-specific-panel .panel-body {
|
||||
border-radius: var(--roundness);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Notifications from 'src/components/notifications/notifications.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
import Notifications from '../notifications/notifications.vue'
|
||||
|
||||
const tabModeDict = {
|
||||
mentions: ['mention'],
|
||||
|
|
@ -17,7 +17,7 @@ const Interactions = {
|
|||
allowFollowingMove:
|
||||
this.$store.state.users.currentUser.allow_following_move,
|
||||
filterMode: tabModeDict.mentions,
|
||||
canSeeReports: this.$store.state.users.currentUser.privileges.includes(
|
||||
canSeeReports: this.$store.state.users.currentUser.privileges.has(
|
||||
'reports_manage_reports',
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@
|
|||
</tab-switcher>
|
||||
<Notifications
|
||||
ref="notifications"
|
||||
:no-heading="true"
|
||||
:no-extra="true"
|
||||
:minimal-mode="true"
|
||||
:filter-mode="filterMode"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import localeService from '../../services/locale/locale.service.js'
|
||||
import Select from '../select/select.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 10em;
|
||||
object-fit: cover;
|
||||
border-radius: var(--roundness);
|
||||
}
|
||||
|
|
|
|||
78
src/components/list/list.css
Normal file
78
src/components/list/list.css
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
.List {
|
||||
--__line-height: 1.5em;
|
||||
--__horizontal-gap: 0.75em;
|
||||
--__vertical-gap: 0.5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.list {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&[aria-expanded="true"] {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-right-radius: var(--roundness);
|
||||
border-top-left-radius: var(--roundness);
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-right-radius: var(--roundness);
|
||||
border-bottom-left-radius: var(--roundness);
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px dotted var(--border);
|
||||
}
|
||||
|
||||
&:empty {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--__vertical-gap) var(--__horizontal-gap);
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: var(--border);
|
||||
|
||||
.actions {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 0.9em;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
padding-right: var(--__horizontal-gap);
|
||||
flex: none;
|
||||
}
|
||||
|
||||
&.-scrollable {
|
||||
overflow-y: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.list {
|
||||
overflow-y: auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/components/list/list.js
Normal file
158
src/components/list/list.js
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import { isEmpty } from 'lodash'
|
||||
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const List = {
|
||||
props: {
|
||||
boxOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
fetchFunction: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
getKey: {
|
||||
type: Function,
|
||||
default: (item) => item.id,
|
||||
},
|
||||
getClass: {
|
||||
type: Function,
|
||||
default: () => '',
|
||||
},
|
||||
preSelect: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
nonInteractive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
externalItems: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['fetchRequested', 'select'],
|
||||
components: {
|
||||
Checkbox,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
selected: new Set(this.preSelect),
|
||||
loading: false,
|
||||
bottomedOut: true,
|
||||
error: null,
|
||||
page: 1,
|
||||
total: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
allKeys() {
|
||||
return new Set(this.finalItems.map(this.getKey))
|
||||
},
|
||||
selectedItems() {
|
||||
return this.items.filter((item) => this.selected.has(this.getKey(item)))
|
||||
},
|
||||
allSelected() {
|
||||
return (
|
||||
this.selected.size !== 0 &&
|
||||
this.selected.size === this.finalItems.length
|
||||
)
|
||||
},
|
||||
noneSelected() {
|
||||
return this.selected.size === 0
|
||||
},
|
||||
someSelected() {
|
||||
return !this.allSelected && !this.noneSelected
|
||||
},
|
||||
finalItems() {
|
||||
return this.externalItems || this.items
|
||||
},
|
||||
},
|
||||
created() {
|
||||
window.addEventListener('scroll', this.scrollLoad)
|
||||
|
||||
if (this.fetchFunction && this.items.length === 0) {
|
||||
this.fetchEntries()
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
window.removeEventListener('scroll', this.scrollLoad)
|
||||
},
|
||||
methods: {
|
||||
fetchEntries() {
|
||||
if (this.loading) return
|
||||
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
this.fetchFunction(this.page)
|
||||
.then((result) => {
|
||||
this.loading = false
|
||||
this.bottomedOut = isEmpty(result.items)
|
||||
if (this.externalItems) return
|
||||
this.page += 1
|
||||
this.total = result.count
|
||||
this.items.push(...result.items)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.loading = false
|
||||
this.error = error
|
||||
console.error('Error loading list data:', error)
|
||||
})
|
||||
},
|
||||
reset() {
|
||||
this.items = []
|
||||
this.page = 1
|
||||
this.total = null
|
||||
this.error = null
|
||||
this.loading = false
|
||||
this.fetchEntries()
|
||||
},
|
||||
scrollLoad(e) {
|
||||
if (this.fetchFunction) {
|
||||
const bodyBRect = document.body.getBoundingClientRect()
|
||||
const height = Math.max(bodyBRect.height, -bodyBRect.y)
|
||||
if (
|
||||
this.$el.offsetHeight > 0 &&
|
||||
window.innerHeight + window.pageYOffset >= height - 750
|
||||
) {
|
||||
this.fetchEntries()
|
||||
}
|
||||
}
|
||||
},
|
||||
isSelected(item) {
|
||||
return this.selected.has(this.getKey(item))
|
||||
},
|
||||
toggle(checked, item) {
|
||||
const key = this.getKey(item)
|
||||
if (checked) {
|
||||
this.selected.add(key)
|
||||
} else {
|
||||
this.selected.delete(key)
|
||||
}
|
||||
|
||||
this.$emit('select', this.selected)
|
||||
},
|
||||
toggleAll(value) {
|
||||
if (value) {
|
||||
this.selected = new Set([...this.allKeys])
|
||||
} else {
|
||||
this.selected = new Set([])
|
||||
}
|
||||
this.$emit('select', this.selected)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default List
|
||||
|
|
@ -1,48 +1,90 @@
|
|||
<template>
|
||||
<div
|
||||
class="list"
|
||||
class="List"
|
||||
role="list"
|
||||
:class="{ '-scrollable': scrollable }"
|
||||
>
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="getKey(item)"
|
||||
class="list-item"
|
||||
:class="[getClass(item), nonInteractive ? '-non-interactive' : '']"
|
||||
role="listitem"
|
||||
v-if="selectable"
|
||||
class="header"
|
||||
>
|
||||
<slot
|
||||
name="item"
|
||||
:item="item"
|
||||
/>
|
||||
<div class="checkbox-wrapper">
|
||||
<Checkbox
|
||||
:model-value="allSelected"
|
||||
:indeterminate="someSelected"
|
||||
@update:model-value="toggleAll"
|
||||
>
|
||||
{{ $t('selectable_list.select_all') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<slot
|
||||
name="header"
|
||||
:selected="selectedItems"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="items.length === 0 && !!$slots.empty"
|
||||
class="list-empty-content faint"
|
||||
class="list"
|
||||
role="list"
|
||||
>
|
||||
<slot name="empty" />
|
||||
<div
|
||||
v-for="item in finalItems"
|
||||
:key="getKey(item)"
|
||||
class="list-item"
|
||||
:class="[getClass(item), nonInteractive ? '-non-interactive' : '']"
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
v-if="selectable"
|
||||
class="checkbox-wrapper"
|
||||
>
|
||||
<Checkbox
|
||||
:model-value="isSelected(item)"
|
||||
@update:model-value="checked => toggle(checked, item)"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
<slot
|
||||
name="item"
|
||||
:item="item"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="finalItems.length === 0 && !!$slots.empty"
|
||||
class="list-item list-empty-content faint"
|
||||
>
|
||||
<slot name="empty" />
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button
|
||||
v-if="error"
|
||||
class="button-unstyled -link -fullwidth alert error"
|
||||
@click="fetchEntries"
|
||||
>
|
||||
{{ $t('general.generic_error') }}
|
||||
</button>
|
||||
<FAIcon
|
||||
v-else-if="loading"
|
||||
spin
|
||||
icon="circle-notch"
|
||||
/>
|
||||
<a
|
||||
v-else-if="!bottomedOut"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="fetchEntries"
|
||||
>
|
||||
{{ $t('general.more') }}
|
||||
</a>
|
||||
<span v-else-if="finalItems.length !== 0">
|
||||
{{ $t('general.no_more') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
getKey: {
|
||||
type: Function,
|
||||
default: (item) => item.id,
|
||||
},
|
||||
getClass: {
|
||||
type: Function,
|
||||
default: () => '',
|
||||
},
|
||||
nonInteractive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<script src="./list.js"></script>
|
||||
|
||||
<style src="./list.css"></style>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import ListsCard from '../lists_card/lists_card.vue'
|
||||
import FolderCard from 'src/components/folder_card/folder_card.vue'
|
||||
|
||||
import { useListsStore } from 'src/stores/lists.js'
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ const Lists = {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
ListsCard,
|
||||
FolderCard,
|
||||
},
|
||||
computed: {
|
||||
lists() {
|
||||
|
|
|
|||
|
|
@ -14,10 +14,12 @@
|
|||
</router-link>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ListsCard
|
||||
<FolderCard
|
||||
v-for="list in lists.slice().reverse()"
|
||||
:key="list"
|
||||
:list="list"
|
||||
:name="list.title"
|
||||
:link="{ name: 'lists-timeline', params: { id: list.id } }"
|
||||
:link-edit="{ name: 'lists-edit', params: { id: list.id } }"
|
||||
class="list-item"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faEllipsisH)
|
||||
|
||||
const ListsCard = {
|
||||
props: ['list'],
|
||||
}
|
||||
|
||||
export default ListsCard
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
<template>
|
||||
<div class="list-card">
|
||||
<router-link
|
||||
:to="{ name: 'lists-timeline', params: { id: list.id } }"
|
||||
class="list-name"
|
||||
>
|
||||
{{ list.title }}
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="{ name: 'lists-edit', params: { id: list.id } }"
|
||||
class="button-list-edit"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="ellipsis-h"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./lists_card.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.list-card {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.list-name {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.list-name,
|
||||
.button-list-edit {
|
||||
margin: 0;
|
||||
padding: 1em;
|
||||
color: var(--link);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
|
||||
import ListsUserSearch from 'src/components/lists_user_search/lists_user_search.vue'
|
||||
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import ListsUserSearch from '../lists_user_search/lists_user_search.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { useListsStore } from 'src/stores/lists.js'
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@
|
|||
</div>
|
||||
<tab-switcher
|
||||
class="list-member-management"
|
||||
:scrollable-tabs="true"
|
||||
scrollable-tabs
|
||||
>
|
||||
<div
|
||||
v-if="id || addedUserIds.size > 0"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { mapState } from 'vuex'
|
|||
import { getListEntries } from 'src/components/navigation/filter.js'
|
||||
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useListsStore } from 'src/stores/lists.js'
|
||||
|
||||
export const ListsMenuContent = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
import Timeline from 'src/components/timeline/timeline.vue'
|
||||
|
||||
import { useListsStore } from 'src/stores/lists.js'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { debounce } from 'lodash'
|
||||
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { mapActions, mapState as mapPiniaState, mapStores } from 'pinia'
|
||||
import { mapActions, mapState as mapPiniaState } from 'pinia'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import oauthApi from '../../services/new_api/oauth.js'
|
||||
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
|
||||
import { getLoginUrl, getTokenWithCredentials } from 'src/api/oauth.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
|
|
@ -35,19 +35,13 @@ const LoginForm = {
|
|||
this.isTokenAuth ? this.submitToken() : this.submitPassword()
|
||||
},
|
||||
submitToken() {
|
||||
const data = {
|
||||
instance: this.server,
|
||||
commit: this.$store.commit,
|
||||
}
|
||||
|
||||
// NOTE: we do not really need the app token, but obtaining a token and
|
||||
// calling verify_credentials is the only way to ensure the app still works.
|
||||
this.ensureAppToken().then(() => {
|
||||
const app = {
|
||||
window.location.href = getLoginUrl({
|
||||
clientId: this.clientId,
|
||||
clientSecret: this.clientSecret,
|
||||
}
|
||||
oauthApi.login({ ...app, ...data })
|
||||
instance: this.server,
|
||||
})
|
||||
})
|
||||
},
|
||||
submitPassword() {
|
||||
|
|
@ -56,37 +50,32 @@ const LoginForm = {
|
|||
// NOTE: we do not really need the app token, but obtaining a token and
|
||||
// calling verify_credentials is the only way to ensure the app still works.
|
||||
this.ensureAppToken().then(() => {
|
||||
const app = {
|
||||
getTokenWithCredentials({
|
||||
clientId: this.clientId,
|
||||
clientSecret: this.clientSecret,
|
||||
}
|
||||
|
||||
oauthApi
|
||||
.getTokenWithCredentials({
|
||||
...app,
|
||||
instance: this.server,
|
||||
username: this.user.username,
|
||||
password: this.user.password,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.error) {
|
||||
if (result.error === 'mfa_required') {
|
||||
this.requireMFA({ settings: result })
|
||||
} else if (result.identifier === 'password_reset_required') {
|
||||
this.$router.push({
|
||||
name: 'password-reset',
|
||||
params: { passwordResetRequested: true },
|
||||
})
|
||||
} else {
|
||||
this.error = result.error
|
||||
this.focusOnPasswordInput()
|
||||
}
|
||||
return
|
||||
}
|
||||
instance: this.server,
|
||||
username: this.user.username,
|
||||
password: this.user.password,
|
||||
})
|
||||
.then(({ data: result }) => {
|
||||
this.login(result).then(() => {
|
||||
this.$router.push({ name: 'friends' })
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.errorData?.error === 'mfa_required') {
|
||||
this.requireMFA({ settings: error })
|
||||
} else if (error.identifier === 'password_reset_required') {
|
||||
this.$router.push({
|
||||
name: 'password-reset',
|
||||
params: { passwordResetRequested: true },
|
||||
})
|
||||
} else {
|
||||
this.error = error
|
||||
this.focusOnPasswordInput()
|
||||
}
|
||||
return
|
||||
})
|
||||
})
|
||||
},
|
||||
clearError() {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import Flash from 'src/components/flash/flash.vue'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import Modal from 'src/components/modal/modal.vue'
|
||||
import GestureService from '../../services/gesture_service/gesture_service'
|
||||
import Modal from '../modal/modal.vue'
|
||||
import PinchZoom from '../pinch_zoom/pinch_zoom.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
import SwipeClick from '../swipe_click/swipe_click.vue'
|
||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||
|
||||
import { useMediaViewerStore } from 'src/stores/media_viewer.js'
|
||||
|
||||
|
|
@ -20,12 +17,17 @@ library.add(faChevronLeft, faChevronRight, faCircleNotch, faTimes)
|
|||
|
||||
const MediaModal = {
|
||||
components: {
|
||||
StillImage,
|
||||
VideoAttachment,
|
||||
PinchZoom,
|
||||
SwipeClick,
|
||||
VideoAttachment: defineAsyncComponent(
|
||||
() => import('src/components/video_attachment/video_attachment.vue'),
|
||||
),
|
||||
PinchZoom: defineAsyncComponent(
|
||||
() => import('src/components/pinch_zoom/pinch_zoom.vue'),
|
||||
),
|
||||
SwipeClick: defineAsyncComponent(
|
||||
() => import('src/components/swipe_click/swipe_click.vue'),
|
||||
),
|
||||
Modal,
|
||||
Flash,
|
||||
Flash: defineAsyncComponent(() => import('src/components/flash/flash.vue')),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -89,13 +89,16 @@
|
|||
/>
|
||||
</button>
|
||||
|
||||
<span
|
||||
<details
|
||||
v-if="description"
|
||||
open
|
||||
class="description"
|
||||
>
|
||||
{{ description }}
|
||||
</span>
|
||||
<summary>{{ $t('status.attachment_description') }}</summary>
|
||||
<span>{{ description }}</span>
|
||||
</details>
|
||||
<span
|
||||
v-if="media.length > 1"
|
||||
class="counter"
|
||||
>
|
||||
{{ $t('media_modal.counter', { current: currentIndex + 1, total: media.length }, currentIndex + 1) }}
|
||||
|
|
@ -159,19 +162,43 @@ $modal-view-button-icon-margin: 0.5em;
|
|||
.counter {
|
||||
/* Hardcoded since background is also hardcoded */
|
||||
color: white;
|
||||
margin-top: 1em;
|
||||
text-shadow: 0 0 10px black, 0 0 10px black;
|
||||
padding: 0.2em 2em;
|
||||
text-shadow: 0 0 1em black, 0 0 1em black, 0 0 1em black;
|
||||
margin: 1em 2em;
|
||||
overflow: hidden;
|
||||
|
||||
}
|
||||
|
||||
.description + .counter {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
flex: 0 0 auto;
|
||||
overflow-y: auto;
|
||||
min-height: 1em;
|
||||
max-width: 35.8em;
|
||||
max-width: 80ch;
|
||||
max-height: 9.5em;
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
|
||||
summary {
|
||||
margin-bottom: 0.5em;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
min-height: 1em;
|
||||
text-wrap: pretty;
|
||||
max-height: 10.5em;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
scrollbar-color: white transparent;
|
||||
|
||||
&::-webkit-scrollbar-button,
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-image {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import UnicodeDomainIndicator from 'src/components/unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||
import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
|
||||
import UserPopover from 'src/components/user_popover/user_popover.vue'
|
||||
import {
|
||||
highlightClass,
|
||||
highlightStyle,
|
||||
} from '../../services/user_highlighter/user_highlighter.js'
|
||||
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import { useUserHighlightStore } from 'src/stores/user_highlight.js'
|
||||
|
|
@ -24,9 +24,7 @@ const MentionLink = {
|
|||
components: {
|
||||
UserAvatar,
|
||||
UnicodeDomainIndicator,
|
||||
UserPopover: defineAsyncComponent(
|
||||
() => import('../user_popover/user_popover.vue'),
|
||||
),
|
||||
UserPopover,
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
import Timeline from 'src/components/timeline/timeline.vue'
|
||||
|
||||
const Mentions = {
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { mapActions, mapState, mapStores } from 'pinia'
|
||||
|
||||
import mfaApi from '../../services/new_api/mfa.js'
|
||||
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
|
||||
import { verifyRecoveryCode } from 'src/api/mfa.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
|
|
@ -43,18 +43,18 @@ export default {
|
|||
code: this.code,
|
||||
}
|
||||
|
||||
mfaApi.verifyRecoveryCode(data).then((result) => {
|
||||
if (result.error) {
|
||||
this.error = result.error
|
||||
verifyRecoveryCode(data)
|
||||
.then((result) => {
|
||||
this.login(result).then(() => {
|
||||
this.$router.push({ name: 'friends' })
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.error = error
|
||||
this.code = null
|
||||
this.focusOnCodeInput()
|
||||
return
|
||||
}
|
||||
|
||||
this.login(result).then(() => {
|
||||
this.$router.push({ name: 'friends' })
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { mapActions, mapState, mapStores } from 'pinia'
|
||||
|
||||
import mfaApi from '../../services/new_api/mfa.js'
|
||||
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
|
||||
import { verifyOTPCode } from 'src/api/mfa.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
|
|
@ -46,18 +46,18 @@ export default {
|
|||
code: this.code,
|
||||
}
|
||||
|
||||
mfaApi.verifyOTPCode(data).then((result) => {
|
||||
if (result.error) {
|
||||
this.error = result.error
|
||||
verifyOTPCode(data)
|
||||
.then(({ data: result }) => {
|
||||
this.login(result).then(() => {
|
||||
this.$router.push({ name: 'friends' })
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.error = error
|
||||
this.code = null
|
||||
this.focusOnCodeInput()
|
||||
return
|
||||
}
|
||||
|
||||
this.login(result).then(() => {
|
||||
this.$router.push({ name: 'friends' })
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue