Merge remote-tracking branch 'origin/develop' into 2-10-1-fixes
This commit is contained in:
commit
dcb7ed1b8c
428 changed files with 55612 additions and 18549 deletions
|
|
@ -1,8 +1,8 @@
|
|||
import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue'
|
||||
import FeaturesPanel from '../features_panel/features_panel.vue'
|
||||
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
|
||||
import StaffPanel from '../staff_panel/staff_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'
|
||||
|
||||
const About = {
|
||||
components: {
|
||||
|
|
@ -10,16 +10,20 @@ const About = {
|
|||
FeaturesPanel,
|
||||
TermsOfServicePanel,
|
||||
StaffPanel,
|
||||
MRFTransparencyPanel
|
||||
MRFTransparencyPanel,
|
||||
},
|
||||
computed: {
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
showInstanceSpecificPanel () {
|
||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
showFeaturesPanel() {
|
||||
return this.$store.state.instance.showFeaturesPanel
|
||||
},
|
||||
showInstanceSpecificPanel() {
|
||||
return (
|
||||
this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
!this.$store.getters.mergedConfig.hideISP &&
|
||||
this.$store.state.instance.instanceSpecificPanelContent
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default About
|
||||
|
|
|
|||
|
|
@ -1,27 +1,23 @@
|
|||
import { mapState } from 'vuex'
|
||||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faEllipsisV
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useReportsStore } from 'src/stores/reports'
|
||||
|
||||
library.add(
|
||||
faEllipsisV
|
||||
)
|
||||
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 { useReportsStore } from 'src/stores/reports'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faEllipsisV)
|
||||
|
||||
const AccountActions = {
|
||||
props: [
|
||||
'user', 'relationship'
|
||||
],
|
||||
data () {
|
||||
props: ['user', 'relationship'],
|
||||
data() {
|
||||
return {
|
||||
showingConfirmBlock: false,
|
||||
showingConfirmRemoveFollower: false
|
||||
showingConfirmRemoveFollower: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
@ -29,25 +25,25 @@ const AccountActions = {
|
|||
Popover,
|
||||
UserListMenu,
|
||||
ConfirmModal,
|
||||
UserTimedFilterModal
|
||||
UserTimedFilterModal,
|
||||
},
|
||||
methods: {
|
||||
showConfirmRemoveUserFromFollowers () {
|
||||
showConfirmRemoveUserFromFollowers() {
|
||||
this.showingConfirmRemoveFollower = true
|
||||
},
|
||||
hideConfirmRemoveUserFromFollowers () {
|
||||
hideConfirmRemoveUserFromFollowers() {
|
||||
this.showingConfirmRemoveFollower = false
|
||||
},
|
||||
hideConfirmBlock () {
|
||||
hideConfirmBlock() {
|
||||
this.showingConfirmBlock = false
|
||||
},
|
||||
showRepeats () {
|
||||
showRepeats() {
|
||||
this.$store.dispatch('showReblogs', this.user.id)
|
||||
},
|
||||
hideRepeats () {
|
||||
hideRepeats() {
|
||||
this.$store.dispatch('hideReblogs', this.user.id)
|
||||
},
|
||||
blockUser () {
|
||||
blockUser() {
|
||||
if (this.$refs.timedBlockDialog) {
|
||||
this.$refs.timedBlockDialog.optionallyPrompt()
|
||||
} else {
|
||||
|
|
@ -58,46 +54,50 @@ const AccountActions = {
|
|||
}
|
||||
}
|
||||
},
|
||||
doBlockUser () {
|
||||
doBlockUser() {
|
||||
this.$store.dispatch('blockUser', { id: this.user.id })
|
||||
this.hideConfirmBlock()
|
||||
},
|
||||
unblockUser () {
|
||||
unblockUser() {
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
},
|
||||
removeUserFromFollowers () {
|
||||
removeUserFromFollowers() {
|
||||
if (!this.shouldConfirmRemoveUserFromFollowers) {
|
||||
this.doRemoveUserFromFollowers()
|
||||
} else {
|
||||
this.showConfirmRemoveUserFromFollowers()
|
||||
}
|
||||
},
|
||||
doRemoveUserFromFollowers () {
|
||||
doRemoveUserFromFollowers() {
|
||||
this.$store.dispatch('removeUserFromFollowers', this.user.id)
|
||||
this.hideConfirmRemoveUserFromFollowers()
|
||||
},
|
||||
reportUser () {
|
||||
reportUser() {
|
||||
useReportsStore().openUserReportingModal({ userId: this.user.id })
|
||||
},
|
||||
openChat () {
|
||||
openChat() {
|
||||
this.$router.push({
|
||||
name: 'chat',
|
||||
params: { username: this.$store.state.users.currentUser.screen_name, recipient_id: this.user.id }
|
||||
params: {
|
||||
username: this.$store.state.users.currentUser.screen_name,
|
||||
recipient_id: this.user.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
shouldConfirmBlock () {
|
||||
shouldConfirmBlock() {
|
||||
return this.$store.getters.mergedConfig.modalOnBlock
|
||||
},
|
||||
shouldConfirmRemoveUserFromFollowers () {
|
||||
shouldConfirmRemoveUserFromFollowers() {
|
||||
return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
|
||||
},
|
||||
...mapState({
|
||||
blockExpirationSupported: state => state.instance.blockExpiration,
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||
})
|
||||
}
|
||||
blockExpirationSupported: (state) => state.instance.blockExpiration,
|
||||
pleromaChatMessagesAvailable: (state) =>
|
||||
state.instance.pleromaChatMessagesAvailable,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export default AccountActions
|
||||
|
|
|
|||
|
|
@ -1,57 +1,51 @@
|
|||
export default {
|
||||
name: 'Alert',
|
||||
selector: '.alert',
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon',
|
||||
'Link',
|
||||
'Border',
|
||||
'ButtonUnstyled'
|
||||
],
|
||||
validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'],
|
||||
variants: {
|
||||
normal: '.neutral',
|
||||
error: '.error',
|
||||
warning: '.warning',
|
||||
success: '.success'
|
||||
success: '.success',
|
||||
},
|
||||
editor: {
|
||||
border: 1,
|
||||
aspect: '3 / 1'
|
||||
aspect: '3 / 1',
|
||||
},
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
background: '--text',
|
||||
opacity: 0.5,
|
||||
blur: '9px'
|
||||
}
|
||||
blur: '9px',
|
||||
},
|
||||
},
|
||||
{
|
||||
parent: {
|
||||
component: 'Alert'
|
||||
component: 'Alert',
|
||||
},
|
||||
component: 'Border',
|
||||
directives: {
|
||||
textColor: '--parent'
|
||||
}
|
||||
textColor: '--parent',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'error',
|
||||
directives: {
|
||||
background: '--cRed'
|
||||
}
|
||||
background: '--cRed',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'warning',
|
||||
directives: {
|
||||
background: '--cOrange'
|
||||
}
|
||||
background: '--cOrange',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'success',
|
||||
directives: {
|
||||
background: '--cGreen'
|
||||
}
|
||||
}
|
||||
]
|
||||
background: '--cGreen',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,109 +1,129 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
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 localeService from '../../services/locale/locale.service.js'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
|
||||
const Announcement = {
|
||||
components: {
|
||||
AnnouncementEditor,
|
||||
RichContent
|
||||
RichContent,
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
editedAnnouncement: {
|
||||
content: '',
|
||||
startsAt: undefined,
|
||||
endsAt: undefined,
|
||||
allDay: undefined
|
||||
allDay: undefined,
|
||||
},
|
||||
editError: ''
|
||||
editError: '',
|
||||
}
|
||||
},
|
||||
props: {
|
||||
announcement: Object
|
||||
announcement: Object,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
}),
|
||||
canEditAnnouncement () {
|
||||
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
|
||||
canEditAnnouncement() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.privileges.includes(
|
||||
'announcements_manage_announcements',
|
||||
)
|
||||
)
|
||||
},
|
||||
content () {
|
||||
content() {
|
||||
return this.announcement.content
|
||||
},
|
||||
isRead () {
|
||||
isRead() {
|
||||
return this.announcement.read
|
||||
},
|
||||
publishedAt () {
|
||||
publishedAt() {
|
||||
const time = this.announcement.published_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
)
|
||||
},
|
||||
startsAt () {
|
||||
startsAt() {
|
||||
const time = this.announcement.starts_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
)
|
||||
},
|
||||
endsAt () {
|
||||
endsAt() {
|
||||
const time = this.announcement.ends_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
)
|
||||
},
|
||||
inactive () {
|
||||
inactive() {
|
||||
return this.announcement.inactive
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
markAsRead () {
|
||||
markAsRead() {
|
||||
if (!this.isRead) {
|
||||
return useAnnouncementsStore().markAnnouncementAsRead(this.announcement.id)
|
||||
return useAnnouncementsStore().markAnnouncementAsRead(
|
||||
this.announcement.id,
|
||||
)
|
||||
}
|
||||
},
|
||||
deleteAnnouncement () {
|
||||
deleteAnnouncement() {
|
||||
return useAnnouncementsStore().deleteAnnouncement(this.announcement.id)
|
||||
},
|
||||
formatTimeOrDate (time, locale) {
|
||||
formatTimeOrDate(time, locale) {
|
||||
const d = new Date(time)
|
||||
return this.announcement.all_day ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
|
||||
return this.announcement.all_day
|
||||
? d.toLocaleDateString(locale)
|
||||
: d.toLocaleString(locale)
|
||||
},
|
||||
enterEditMode () {
|
||||
enterEditMode() {
|
||||
this.editedAnnouncement.content = this.announcement.pleroma.raw_content
|
||||
this.editedAnnouncement.startsAt = this.announcement.starts_at
|
||||
this.editedAnnouncement.endsAt = this.announcement.ends_at
|
||||
this.editedAnnouncement.allDay = this.announcement.all_day
|
||||
this.editing = true
|
||||
},
|
||||
submitEdit () {
|
||||
useAnnouncementsStore().editAnnouncement({
|
||||
id: this.announcement.id,
|
||||
...this.editedAnnouncement
|
||||
})
|
||||
submitEdit() {
|
||||
useAnnouncementsStore()
|
||||
.editAnnouncement({
|
||||
id: this.announcement.id,
|
||||
...this.editedAnnouncement,
|
||||
})
|
||||
.then(() => {
|
||||
this.editing = false
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
this.editError = error.error
|
||||
})
|
||||
},
|
||||
cancelEdit () {
|
||||
cancelEdit() {
|
||||
this.editing = false
|
||||
},
|
||||
clearError () {
|
||||
clearError() {
|
||||
this.editError = undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default Announcement
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import Checkbox from '../checkbox/checkbox.vue'
|
|||
|
||||
const AnnouncementEditor = {
|
||||
components: {
|
||||
Checkbox
|
||||
Checkbox,
|
||||
},
|
||||
props: {
|
||||
announcement: Object,
|
||||
disabled: Boolean
|
||||
}
|
||||
disabled: Boolean,
|
||||
},
|
||||
}
|
||||
|
||||
export default AnnouncementEditor
|
||||
|
|
|
|||
|
|
@ -1,59 +1,66 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import Announcement from '../announcement/announcement.vue'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
|
||||
const AnnouncementsPage = {
|
||||
components: {
|
||||
Announcement,
|
||||
AnnouncementEditor
|
||||
AnnouncementEditor,
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
newAnnouncement: {
|
||||
content: '',
|
||||
startsAt: undefined,
|
||||
endsAt: undefined,
|
||||
allDay: false
|
||||
allDay: false,
|
||||
},
|
||||
posting: false,
|
||||
error: undefined
|
||||
error: undefined,
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
useAnnouncementsStore().fetchAnnouncements()
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
}),
|
||||
announcements () {
|
||||
announcements() {
|
||||
return useAnnouncementsStore().announcements
|
||||
},
|
||||
canPostAnnouncement () {
|
||||
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
|
||||
}
|
||||
canPostAnnouncement() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.privileges.includes(
|
||||
'announcements_manage_announcements',
|
||||
)
|
||||
)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
postAnnouncement () {
|
||||
postAnnouncement() {
|
||||
this.posting = true
|
||||
useAnnouncementsStore().postAnnouncement(this.newAnnouncement)
|
||||
useAnnouncementsStore()
|
||||
.postAnnouncement(this.newAnnouncement)
|
||||
.then(() => {
|
||||
this.newAnnouncement.content = ''
|
||||
this.startsAt = undefined
|
||||
this.endsAt = undefined
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
this.error = error.error
|
||||
})
|
||||
.finally(() => {
|
||||
this.posting = false
|
||||
})
|
||||
},
|
||||
clearError () {
|
||||
clearError() {
|
||||
this.error = undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default AnnouncementsPage
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@
|
|||
export default {
|
||||
emits: ['resetAsyncComponent'],
|
||||
methods: {
|
||||
retry () {
|
||||
retry() {
|
||||
this.$emit('resetAsyncComponent')
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,26 @@
|
|||
import StillImage from '../still-image/still-image.vue'
|
||||
import Flash from '../flash/flash.vue'
|
||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import { useMediaViewerStore } from 'src/stores/media_viewer'
|
||||
import nsfwImage from '../../assets/nsfw.png'
|
||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||
import { mapGetters } from 'vuex'
|
||||
import Flash from '../flash/flash.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faAlignRight,
|
||||
faFile,
|
||||
faMusic,
|
||||
faImage,
|
||||
faVideo,
|
||||
faPlayCircle,
|
||||
faTimes,
|
||||
faStop,
|
||||
faSearchPlus,
|
||||
faTrashAlt,
|
||||
faMusic,
|
||||
faPencilAlt,
|
||||
faAlignRight
|
||||
faPlayCircle,
|
||||
faSearchPlus,
|
||||
faStop,
|
||||
faTimes,
|
||||
faTrashAlt,
|
||||
faVideo,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useMediaViewerStore } from 'src/stores/media_viewer'
|
||||
|
||||
library.add(
|
||||
faFile,
|
||||
|
|
@ -31,7 +33,7 @@ library.add(
|
|||
faSearchPlus,
|
||||
faTrashAlt,
|
||||
faPencilAlt,
|
||||
faAlignRight
|
||||
faAlignRight,
|
||||
)
|
||||
|
||||
const Attachment = {
|
||||
|
|
@ -46,72 +48,74 @@ const Attachment = {
|
|||
'remove',
|
||||
'shiftUp',
|
||||
'shiftDn',
|
||||
'edit'
|
||||
'edit',
|
||||
],
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
localDescription: this.description || this.attachment.description,
|
||||
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
||||
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
|
||||
preloadImage: this.$store.getters.mergedConfig.preloadImage,
|
||||
loading: false,
|
||||
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
|
||||
img:
|
||||
fileTypeService.fileType(this.attachment.mimetype) === 'image' &&
|
||||
document.createElement('img'),
|
||||
modalOpen: false,
|
||||
showHidden: false,
|
||||
flashLoaded: false,
|
||||
showDescription: false
|
||||
showDescription: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Flash,
|
||||
StillImage,
|
||||
VideoAttachment
|
||||
VideoAttachment,
|
||||
},
|
||||
computed: {
|
||||
classNames () {
|
||||
classNames() {
|
||||
return [
|
||||
{
|
||||
'-loading': this.loading,
|
||||
'-nsfw-placeholder': this.hidden,
|
||||
'-editable': this.edit !== undefined,
|
||||
'-compact': this.compact
|
||||
'-compact': this.compact,
|
||||
},
|
||||
'-type-' + this.type,
|
||||
this.size && '-size-' + this.size,
|
||||
`-${this.useContainFit ? 'contain' : 'cover'}-fit`
|
||||
`-${this.useContainFit ? 'contain' : 'cover'}-fit`,
|
||||
]
|
||||
},
|
||||
usePlaceholder () {
|
||||
usePlaceholder() {
|
||||
return this.size === 'hide'
|
||||
},
|
||||
useContainFit () {
|
||||
useContainFit() {
|
||||
return this.$store.getters.mergedConfig.useContainFit
|
||||
},
|
||||
placeholderName () {
|
||||
placeholderName() {
|
||||
if (this.attachment.description === '' || !this.attachment.description) {
|
||||
return this.type.toUpperCase()
|
||||
}
|
||||
return this.attachment.description
|
||||
},
|
||||
placeholderIconClass () {
|
||||
placeholderIconClass() {
|
||||
if (this.type === 'image') return 'image'
|
||||
if (this.type === 'video') return 'video'
|
||||
if (this.type === 'audio') return 'music'
|
||||
return 'file'
|
||||
},
|
||||
referrerpolicy () {
|
||||
referrerpolicy() {
|
||||
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
||||
},
|
||||
type () {
|
||||
type() {
|
||||
return fileTypeService.fileType(this.attachment.mimetype)
|
||||
},
|
||||
hidden () {
|
||||
hidden() {
|
||||
return this.nsfw && this.hideNsfwLocal && !this.showHidden
|
||||
},
|
||||
isEmpty () {
|
||||
return (this.type === 'html' && !this.attachment.oembed)
|
||||
isEmpty() {
|
||||
return this.type === 'html' && !this.attachment.oembed
|
||||
},
|
||||
useModal () {
|
||||
useModal() {
|
||||
let modalTypes = []
|
||||
switch (this.size) {
|
||||
case 'hide':
|
||||
|
|
@ -126,26 +130,26 @@ const Attachment = {
|
|||
}
|
||||
return modalTypes.includes(this.type)
|
||||
},
|
||||
videoTag () {
|
||||
videoTag() {
|
||||
return this.useModal ? 'button' : 'span'
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
...mapGetters(['mergedConfig']),
|
||||
},
|
||||
watch: {
|
||||
'attachment.description' (newVal) {
|
||||
'attachment.description'(newVal) {
|
||||
this.localDescription = newVal
|
||||
},
|
||||
localDescription (newVal) {
|
||||
localDescription(newVal) {
|
||||
this.onEdit(newVal)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
linkClicked ({ target }) {
|
||||
linkClicked({ target }) {
|
||||
if (target.tagName === 'A') {
|
||||
window.open(target.href, '_blank')
|
||||
}
|
||||
},
|
||||
openModal () {
|
||||
openModal() {
|
||||
if (this.useModal) {
|
||||
this.$emit('setMedia')
|
||||
useMediaViewerStore().setCurrentMedia(this.attachment)
|
||||
|
|
@ -153,34 +157,35 @@ const Attachment = {
|
|||
window.open(this.attachment.url)
|
||||
}
|
||||
},
|
||||
openModalForce () {
|
||||
openModalForce() {
|
||||
this.$emit('setMedia')
|
||||
useMediaViewerStore().setCurrentMedia(this.attachment)
|
||||
},
|
||||
onEdit (event) {
|
||||
onEdit(event) {
|
||||
this.edit && this.edit(this.attachment, event)
|
||||
},
|
||||
onRemove () {
|
||||
onRemove() {
|
||||
this.remove && this.remove(this.attachment)
|
||||
},
|
||||
onShiftUp () {
|
||||
onShiftUp() {
|
||||
this.shiftUp && this.shiftUp(this.attachment)
|
||||
},
|
||||
onShiftDn () {
|
||||
onShiftDn() {
|
||||
this.shiftDn && this.shiftDn(this.attachment)
|
||||
},
|
||||
stopFlash () {
|
||||
stopFlash() {
|
||||
this.$refs.flash.closePlayer()
|
||||
},
|
||||
setFlashLoaded (event) {
|
||||
setFlashLoaded(event) {
|
||||
this.flashLoaded = event
|
||||
},
|
||||
toggleDescription () {
|
||||
toggleDescription() {
|
||||
this.showDescription = !this.showDescription
|
||||
},
|
||||
toggleHidden (event) {
|
||||
toggleHidden(event) {
|
||||
if (
|
||||
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
|
||||
this.mergedConfig.useOneClickNsfw &&
|
||||
!this.showHidden &&
|
||||
(this.type !== 'video' || this.mergedConfig.playVideosInModal)
|
||||
) {
|
||||
this.openModal(event)
|
||||
|
|
@ -201,12 +206,12 @@ const Attachment = {
|
|||
this.showHidden = !this.showHidden
|
||||
}
|
||||
},
|
||||
onImageLoad (image) {
|
||||
onImageLoad(image) {
|
||||
const width = image.naturalWidth
|
||||
const height = image.naturalHeight
|
||||
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default Attachment
|
||||
|
|
|
|||
|
|
@ -1,28 +1,33 @@
|
|||
import { mapState } from 'pinia'
|
||||
import { h, resolveComponent } from 'vue'
|
||||
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow'
|
||||
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 { mapState } from 'pinia'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow'
|
||||
|
||||
const AuthForm = {
|
||||
name: 'AuthForm',
|
||||
render () {
|
||||
render() {
|
||||
return h(resolveComponent(this.authForm))
|
||||
},
|
||||
computed: {
|
||||
authForm () {
|
||||
if (this.requiredTOTP) { return 'MFATOTPForm' }
|
||||
if (this.requiredRecovery) { return 'MFARecoveryForm' }
|
||||
authForm() {
|
||||
if (this.requiredTOTP) {
|
||||
return 'MFATOTPForm'
|
||||
}
|
||||
if (this.requiredRecovery) {
|
||||
return 'MFARecoveryForm'
|
||||
}
|
||||
return 'LoginForm'
|
||||
},
|
||||
...mapState(useAuthFlowStore, ['requiredTOTP', 'requiredRecovery'])
|
||||
...mapState(useAuthFlowStore, ['requiredTOTP', 'requiredRecovery']),
|
||||
},
|
||||
components: {
|
||||
MFARecoveryForm,
|
||||
MFATOTPForm,
|
||||
LoginForm
|
||||
}
|
||||
LoginForm,
|
||||
},
|
||||
}
|
||||
|
||||
export default AuthForm
|
||||
|
|
|
|||
|
|
@ -2,51 +2,55 @@ const debounceMilliseconds = 500
|
|||
|
||||
export default {
|
||||
props: {
|
||||
query: { // function to query results and return a promise
|
||||
query: {
|
||||
// function to query results and return a promise
|
||||
type: Function,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
filter: { // function to filter results in real time
|
||||
type: Function
|
||||
filter: {
|
||||
// function to filter results in real time
|
||||
type: Function,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Search...'
|
||||
}
|
||||
default: 'Search...',
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
term: '',
|
||||
timeout: null,
|
||||
results: [],
|
||||
resultsVisible: false
|
||||
resultsVisible: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filtered () {
|
||||
filtered() {
|
||||
return this.filter ? this.filter(this.results) : this.results
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
term (val) {
|
||||
term(val) {
|
||||
this.fetchResults(val)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchResults (term) {
|
||||
fetchResults(term) {
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = setTimeout(() => {
|
||||
this.results = []
|
||||
if (term) {
|
||||
this.query(term).then((results) => { this.results = results })
|
||||
this.query(term).then((results) => {
|
||||
this.results = results
|
||||
})
|
||||
}
|
||||
}, debounceMilliseconds)
|
||||
},
|
||||
onInputClick () {
|
||||
onInputClick() {
|
||||
this.resultsVisible = true
|
||||
},
|
||||
onClickOutside () {
|
||||
onClickOutside() {
|
||||
this.resultsVisible = false
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,25 @@
|
|||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
|
||||
const AvatarList = {
|
||||
props: ['users'],
|
||||
computed: {
|
||||
slicedUsers () {
|
||||
slicedUsers() {
|
||||
return this.users ? this.users.slice(0, 15) : []
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
UserAvatar
|
||||
UserAvatar,
|
||||
},
|
||||
methods: {
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
}
|
||||
userProfileLink(user) {
|
||||
return generateProfileLink(
|
||||
user.id,
|
||||
user.screen_name,
|
||||
this.$store.state.instance.restrictedNicknames,
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default AvatarList
|
||||
|
|
|
|||
|
|
@ -1,30 +1,27 @@
|
|||
export default {
|
||||
name: 'Badge',
|
||||
selector: '.badge',
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon'
|
||||
],
|
||||
validInnerComponents: ['Text', 'Icon'],
|
||||
variants: {
|
||||
notification: '.-notification'
|
||||
notification: '.-notification',
|
||||
},
|
||||
defaultRules: [
|
||||
{
|
||||
component: 'Root',
|
||||
directives: {
|
||||
'--badgeNotification': 'color | --cRed'
|
||||
}
|
||||
'--badgeNotification': 'color | --cRed',
|
||||
},
|
||||
},
|
||||
{
|
||||
directives: {
|
||||
background: '--cGreen'
|
||||
}
|
||||
background: '--cGreen',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'notification',
|
||||
directives: {
|
||||
background: '--cRed'
|
||||
}
|
||||
}
|
||||
]
|
||||
background: '--cRed',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,26 @@
|
|||
import UserPopover from '../user_popover/user_popover.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserLink from '../user_link/user_link.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserLink from '../user_link/user_link.vue'
|
||||
import UserPopover from '../user_popover/user_popover.vue'
|
||||
|
||||
const BasicUserCard = {
|
||||
props: [
|
||||
'user'
|
||||
],
|
||||
props: ['user'],
|
||||
components: {
|
||||
UserPopover,
|
||||
UserAvatar,
|
||||
RichContent,
|
||||
UserLink
|
||||
UserLink,
|
||||
},
|
||||
methods: {
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
}
|
||||
userProfileLink(user) {
|
||||
return generateProfileLink(
|
||||
user.id,
|
||||
user.screen_name,
|
||||
this.$store.state.instance.restrictedNicknames,
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default BasicUserCard
|
||||
|
|
|
|||
|
|
@ -5,42 +5,44 @@ import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
|||
const BlockCard = {
|
||||
props: ['userId'],
|
||||
computed: {
|
||||
user () {
|
||||
user() {
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
relationship () {
|
||||
relationship() {
|
||||
return this.$store.getters.relationship(this.userId)
|
||||
},
|
||||
blocked () {
|
||||
blocked() {
|
||||
return this.relationship.blocking
|
||||
},
|
||||
blockExpiryAvailable () {
|
||||
blockExpiryAvailable() {
|
||||
return Object.hasOwn(this.user, 'block_expires_at')
|
||||
},
|
||||
blockExpiry () {
|
||||
blockExpiry() {
|
||||
return this.user.block_expires_at === false
|
||||
? this.$t('user_card.block_expires_forever')
|
||||
: this.$t('user_card.block_expires_at', [new Date(this.user.mute_expires_at).toLocaleString()])
|
||||
: this.$t('user_card.block_expires_at', [
|
||||
new Date(this.user.mute_expires_at).toLocaleString(),
|
||||
])
|
||||
},
|
||||
...mapState({
|
||||
blockExpirationSupported: state => state.instance.blockExpiration,
|
||||
})
|
||||
blockExpirationSupported: (state) => state.instance.blockExpiration,
|
||||
}),
|
||||
},
|
||||
components: {
|
||||
BasicUserCard
|
||||
BasicUserCard,
|
||||
},
|
||||
methods: {
|
||||
unblockUser () {
|
||||
unblockUser() {
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
},
|
||||
blockUser () {
|
||||
blockUser() {
|
||||
if (this.blockExpirationSupported) {
|
||||
this.$refs.timedBlockDialog.optionallyPrompt()
|
||||
} else {
|
||||
this.$store.dispatch('blockUser', { id: this.user.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default BlockCard
|
||||
|
|
|
|||
|
|
@ -1,22 +1,15 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faEllipsisH
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faEllipsisH
|
||||
)
|
||||
library.add(faEllipsisH)
|
||||
|
||||
const BookmarkFolderCard = {
|
||||
props: [
|
||||
'folder',
|
||||
'allBookmarks'
|
||||
],
|
||||
props: ['folder', 'allBookmarks'],
|
||||
computed: {
|
||||
firstLetter () {
|
||||
firstLetter() {
|
||||
return this.folder ? this.folder.name[0] : null
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default BookmarkFolderCard
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||
import apiService from '../../services/api/api.service'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import apiService from '../../services/api/api.service'
|
||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||
|
||||
const BookmarkFolderEdit = {
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
nameDraft: '',
|
||||
|
|
@ -13,54 +13,59 @@ const BookmarkFolderEdit = {
|
|||
emojiDraft: '',
|
||||
emojiUrlDraft: null,
|
||||
emojiPickerExpanded: false,
|
||||
reallyDelete: false
|
||||
reallyDelete: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
EmojiPicker
|
||||
EmojiPicker,
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
if (!this.id) return
|
||||
const credentials = this.$store.state.users.currentUser.credentials
|
||||
apiService.fetchBookmarkFolders({ credentials })
|
||||
.then((folders) => {
|
||||
const folder = folders.find(folder => folder.id === this.id)
|
||||
if (!folder) return
|
||||
apiService.fetchBookmarkFolders({ credentials }).then((folders) => {
|
||||
const folder = folders.find((folder) => folder.id === this.id)
|
||||
if (!folder) return
|
||||
|
||||
this.nameDraft = this.name = folder.name
|
||||
this.emojiDraft = this.emoji = folder.emoji
|
||||
this.emojiUrlDraft = this.emojiUrl = folder.emoji_url
|
||||
})
|
||||
this.nameDraft = this.name = folder.name
|
||||
this.emojiDraft = this.emoji = folder.emoji
|
||||
this.emojiUrlDraft = this.emojiUrl = folder.emoji_url
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
id () {
|
||||
id() {
|
||||
return this.$route.params.id
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectEmoji (event) {
|
||||
selectEmoji(event) {
|
||||
this.emojiDraft = event.insertion
|
||||
this.emojiUrlDraft = event.insertionUrl
|
||||
},
|
||||
showEmojiPicker () {
|
||||
showEmojiPicker() {
|
||||
if (!this.emojiPickerExpanded) {
|
||||
this.$refs.picker.showPicker()
|
||||
}
|
||||
},
|
||||
onShowPicker () {
|
||||
onShowPicker() {
|
||||
this.emojiPickerExpanded = true
|
||||
},
|
||||
onClosePicker () {
|
||||
onClosePicker() {
|
||||
this.emojiPickerExpanded = false
|
||||
},
|
||||
updateFolder () {
|
||||
useBookmarkFoldersStore().updateBookmarkFolder({ folderId: this.id, name: this.nameDraft, emoji: this.emojiDraft })
|
||||
updateFolder() {
|
||||
useBookmarkFoldersStore()
|
||||
.updateBookmarkFolder({
|
||||
folderId: this.id,
|
||||
name: this.nameDraft,
|
||||
emoji: this.emojiDraft,
|
||||
})
|
||||
.then(() => {
|
||||
this.$router.push({ name: 'bookmark-folders' })
|
||||
})
|
||||
},
|
||||
createFolder () {
|
||||
useBookmarkFoldersStore().createBookmarkFolder({ name: this.nameDraft, emoji: this.emojiDraft })
|
||||
createFolder() {
|
||||
useBookmarkFoldersStore()
|
||||
.createBookmarkFolder({ name: this.nameDraft, emoji: this.emojiDraft })
|
||||
.then(() => {
|
||||
this.$router.push({ name: 'bookmark-folders' })
|
||||
})
|
||||
|
|
@ -68,15 +73,15 @@ const BookmarkFolderEdit = {
|
|||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'bookmark_folders.error',
|
||||
messageArgs: [e.message],
|
||||
level: 'error'
|
||||
level: 'error',
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteFolder () {
|
||||
deleteFolder() {
|
||||
useBookmarkFoldersStore().deleteBookmarkFolder({ folderId: this.id })
|
||||
this.$router.push({ name: 'bookmark-folders' })
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default BookmarkFolderEdit
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue'
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
|
||||
import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue'
|
||||
|
||||
const BookmarkFolders = {
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
isNew: false
|
||||
isNew: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BookmarkFolderCard
|
||||
BookmarkFolderCard,
|
||||
},
|
||||
computed: {
|
||||
bookmarkFolders () {
|
||||
bookmarkFolders() {
|
||||
return useBookmarkFoldersStore().allFolders
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
cancelNewFolder () {
|
||||
cancelNewFolder() {
|
||||
this.isNew = false
|
||||
},
|
||||
newFolder () {
|
||||
newFolder() {
|
||||
this.isNew = true
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default BookmarkFolders
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
import { mapState } from 'pinia'
|
||||
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||
|
||||
import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js'
|
||||
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
|
||||
|
||||
export const BookmarkFoldersMenuContent = {
|
||||
props: [
|
||||
'showPin'
|
||||
],
|
||||
props: ['showPin'],
|
||||
components: {
|
||||
NavigationEntry
|
||||
NavigationEntry,
|
||||
},
|
||||
computed: {
|
||||
...mapState(useBookmarkFoldersStore, {
|
||||
folders: getBookmarkFolderEntries
|
||||
})
|
||||
}
|
||||
folders: getBookmarkFolderEntries,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export default BookmarkFoldersMenuContent
|
||||
|
|
|
|||
|
|
@ -1,32 +1,38 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
|
||||
const Bookmarks = {
|
||||
created () {
|
||||
created() {
|
||||
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
|
||||
this.$store.dispatch('startFetchingTimeline', {
|
||||
timeline: 'bookmarks',
|
||||
bookmarkFolderId: this.folderId || null,
|
||||
})
|
||||
},
|
||||
components: {
|
||||
Timeline
|
||||
Timeline,
|
||||
},
|
||||
computed: {
|
||||
folderId () {
|
||||
folderId() {
|
||||
return this.$route.params.id
|
||||
},
|
||||
timeline () {
|
||||
timeline() {
|
||||
return this.$store.state.statuses.timelines.bookmarks
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
folderId () {
|
||||
folderId() {
|
||||
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
|
||||
}
|
||||
this.$store.dispatch('startFetchingTimeline', {
|
||||
timeline: 'bookmarks',
|
||||
bookmarkFolderId: this.folderId || null,
|
||||
})
|
||||
},
|
||||
},
|
||||
unmounted () {
|
||||
unmounted() {
|
||||
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default Bookmarks
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ export default {
|
|||
{
|
||||
directives: {
|
||||
textColor: '$mod(--parent 10)',
|
||||
textAuto: 'no-auto'
|
||||
}
|
||||
}
|
||||
]
|
||||
textAuto: 'no-auto',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
|
||||
const BubbleTimeline = {
|
||||
components: {
|
||||
Timeline
|
||||
Timeline,
|
||||
},
|
||||
computed: {
|
||||
timeline () { return this.$store.state.statuses.timelines.bubble }
|
||||
timeline() {
|
||||
return this.$store.state.statuses.timelines.bubble
|
||||
},
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
|
||||
},
|
||||
unmounted () {
|
||||
unmounted() {
|
||||
this.$store.dispatch('stopFetchingTimeline', 'bubble')
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
export default BubbleTimeline
|
||||
|
|
|
|||
|
|
@ -12,25 +12,22 @@ export default {
|
|||
focused: ':focus-within',
|
||||
pressed: ':active',
|
||||
hover: ':is(:hover, :focus-visible):not(:disabled)',
|
||||
disabled: ':disabled'
|
||||
disabled: ':disabled',
|
||||
},
|
||||
// Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it.
|
||||
variants: {
|
||||
// Variants save on computation time since adding new variant just adds one more "set".
|
||||
// normal: '', // you can override normal variant, it will be appenended to the main class
|
||||
danger: '.-danger',
|
||||
transparent: '.-transparent'
|
||||
transparent: '.-transparent',
|
||||
// Overall the compuation difficulty is N*((1/6)M^3+M) where M is number of distinct states and N is number of variants.
|
||||
// This (currently) is further multipled by number of places where component can exist.
|
||||
},
|
||||
editor: {
|
||||
aspect: '2 / 1'
|
||||
aspect: '2 / 1',
|
||||
},
|
||||
// This lists all other components that can possibly exist within one. Recursion is currently not supported (and probably won't be supported ever).
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon'
|
||||
],
|
||||
validInnerComponents: ['Text', 'Icon'],
|
||||
// Default rules, used as "default theme", essentially.
|
||||
defaultRules: [
|
||||
{
|
||||
|
|
@ -39,9 +36,11 @@ export default {
|
|||
'--buttonDefaultHoverGlow': 'shadow | 0 0 1 2 --text / 0.4',
|
||||
'--buttonDefaultFocusGlow': 'shadow | 0 0 1 2 --link / 0.5',
|
||||
'--buttonDefaultShadow': 'shadow | 0 0 2 #000000',
|
||||
'--buttonDefaultBevel': 'shadow | $borderSide(#FFFFFF top 0.2 1), $borderSide(#000000 bottom 0.2 1)',
|
||||
'--buttonPressedBevel': 'shadow | inset 0 0 4 #000000, $borderSide(#FFFFFF bottom 0.2 1), $borderSide(#000000 top 0.2 1)'
|
||||
}
|
||||
'--buttonDefaultBevel':
|
||||
'shadow | $borderSide(#FFFFFF top 0.2 1), $borderSide(#000000 bottom 0.2 1)',
|
||||
'--buttonPressedBevel':
|
||||
'shadow | inset 0 0 4 #000000, $borderSide(#FFFFFF bottom 0.2 1), $borderSide(#000000 top 0.2 1)',
|
||||
},
|
||||
},
|
||||
{
|
||||
// component: 'Button', // no need to specify components every time unless you're specifying how other component should look
|
||||
|
|
@ -49,128 +48,128 @@ export default {
|
|||
directives: {
|
||||
background: '--fg',
|
||||
shadow: ['--buttonDefaultShadow', '--buttonDefaultBevel'],
|
||||
roundness: 3
|
||||
}
|
||||
roundness: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'danger',
|
||||
directives: {
|
||||
background: '--cRed'
|
||||
}
|
||||
background: '--cRed',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'transparent',
|
||||
directives: {
|
||||
opacity: 0.5
|
||||
}
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Text',
|
||||
parent: {
|
||||
component: 'Button',
|
||||
variant: 'transparent'
|
||||
variant: 'transparent',
|
||||
},
|
||||
directives: {
|
||||
textColor: '--text'
|
||||
}
|
||||
textColor: '--text',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'Button',
|
||||
variant: 'transparent'
|
||||
variant: 'transparent',
|
||||
},
|
||||
directives: {
|
||||
textColor: '--text'
|
||||
}
|
||||
textColor: '--text',
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['hover'],
|
||||
directives: {
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonDefaultBevel']
|
||||
}
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonDefaultBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['focused'],
|
||||
directives: {
|
||||
shadow: ['--buttonDefaultFocusGlow', '--buttonDefaultBevel']
|
||||
}
|
||||
shadow: ['--buttonDefaultFocusGlow', '--buttonDefaultBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['pressed'],
|
||||
directives: {
|
||||
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel']
|
||||
}
|
||||
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['pressed', 'hover'],
|
||||
directives: {
|
||||
shadow: ['--buttonPressedBevel', '--buttonDefaultHoverGlow']
|
||||
}
|
||||
shadow: ['--buttonPressedBevel', '--buttonDefaultHoverGlow'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['toggled'],
|
||||
directives: {
|
||||
background: '--accent,-24.2',
|
||||
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel']
|
||||
}
|
||||
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'hover'],
|
||||
directives: {
|
||||
background: '--accent,-24.2',
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
|
||||
}
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'focused'],
|
||||
directives: {
|
||||
background: '--accent,-24.2',
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
|
||||
}
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'hover', 'focused'],
|
||||
directives: {
|
||||
background: '--accent,-24.2',
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
|
||||
}
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'disabled'],
|
||||
directives: {
|
||||
background: '$blend(--accent 0.25 --parent)',
|
||||
shadow: ['--buttonPressedBevel']
|
||||
}
|
||||
shadow: ['--buttonPressedBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['disabled'],
|
||||
directives: {
|
||||
background: '$blend(--inheritedBackground 0.25 --parent)',
|
||||
shadow: ['--buttonDefaultBevel']
|
||||
}
|
||||
shadow: ['--buttonDefaultBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Text',
|
||||
parent: {
|
||||
component: 'Button',
|
||||
state: ['disabled']
|
||||
state: ['disabled'],
|
||||
},
|
||||
directives: {
|
||||
textOpacity: 0.25,
|
||||
textOpacityMode: 'blend'
|
||||
}
|
||||
textOpacityMode: 'blend',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'Button',
|
||||
state: ['disabled']
|
||||
state: ['disabled'],
|
||||
},
|
||||
directives: {
|
||||
textOpacity: 0.25,
|
||||
textOpacityMode: 'blend'
|
||||
}
|
||||
}
|
||||
]
|
||||
textOpacityMode: 'blend',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,91 +7,86 @@ export default {
|
|||
toggled: '.toggled',
|
||||
disabled: ':disabled',
|
||||
hover: ':is(:hover, :focus-visible):not(:disabled)',
|
||||
focused: ':focus-within:not(:is(:focus-visible))'
|
||||
focused: ':focus-within:not(:is(:focus-visible))',
|
||||
},
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Link',
|
||||
'Icon',
|
||||
'Badge'
|
||||
],
|
||||
validInnerComponents: ['Text', 'Link', 'Icon', 'Badge'],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
shadow: []
|
||||
}
|
||||
shadow: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['hover']
|
||||
state: ['hover'],
|
||||
},
|
||||
directives: {
|
||||
textColor: '--parent--text'
|
||||
}
|
||||
textColor: '--parent--text',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['toggled']
|
||||
state: ['toggled'],
|
||||
},
|
||||
directives: {
|
||||
textColor: '--parent--text'
|
||||
}
|
||||
textColor: '--parent--text',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['toggled', 'hover']
|
||||
state: ['toggled', 'hover'],
|
||||
},
|
||||
directives: {
|
||||
textColor: '--parent--text'
|
||||
}
|
||||
textColor: '--parent--text',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['toggled', 'focused']
|
||||
state: ['toggled', 'focused'],
|
||||
},
|
||||
directives: {
|
||||
textColor: '--parent--text'
|
||||
}
|
||||
textColor: '--parent--text',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['toggled', 'focused', 'hover']
|
||||
state: ['toggled', 'focused', 'hover'],
|
||||
},
|
||||
directives: {
|
||||
textColor: '--parent--text'
|
||||
}
|
||||
textColor: '--parent--text',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Text',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['disabled']
|
||||
state: ['disabled'],
|
||||
},
|
||||
directives: {
|
||||
textOpacity: 0.25,
|
||||
textOpacityMode: 'blend'
|
||||
}
|
||||
textOpacityMode: 'blend',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['disabled']
|
||||
state: ['disabled'],
|
||||
},
|
||||
directives: {
|
||||
textOpacity: 0.25,
|
||||
textOpacityMode: 'blend'
|
||||
}
|
||||
}
|
||||
]
|
||||
textOpacityMode: 'blend',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,26 @@
|
|||
import _ from 'lodash'
|
||||
import { WSConnectionStatus } from '../../services/api/api.service.js'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import ChatMessage from '../chat_message/chat_message.vue'
|
||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import ChatTitle from '../chat_title/chat_title.vue'
|
||||
import chatService from '../../services/chat_service/chat_service.js'
|
||||
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
|
||||
import { getScrollPosition, getNewTopPosition, isBottomedOut, isScrollable } from './chat_layout_utils.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faChevronDown,
|
||||
faChevronLeft
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
library.add(
|
||||
faChevronDown,
|
||||
faChevronLeft
|
||||
)
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { WSConnectionStatus } from '../../services/api/api.service.js'
|
||||
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,
|
||||
isBottomedOut,
|
||||
isScrollable,
|
||||
} from './chat_layout_utils.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faChevronDown, faChevronLeft)
|
||||
|
||||
const BOTTOMED_OUT_OFFSET = 10
|
||||
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 10
|
||||
|
|
@ -31,78 +32,95 @@ const Chat = {
|
|||
components: {
|
||||
ChatMessage,
|
||||
ChatTitle,
|
||||
PostStatusForm
|
||||
PostStatusForm,
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
jumpToBottomButtonVisible: false,
|
||||
hoveredMessageChainId: undefined,
|
||||
lastScrollPosition: {},
|
||||
scrollableContainerHeight: '100%',
|
||||
errorLoadingChat: false,
|
||||
messageRetriers: {}
|
||||
messageRetriers: {},
|
||||
}
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
this.startFetching()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
window.addEventListener('scroll', this.handleScroll)
|
||||
if (typeof document.hidden !== 'undefined') {
|
||||
document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
|
||||
document.addEventListener(
|
||||
'visibilitychange',
|
||||
this.handleVisibilityChange,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.handleResize()
|
||||
})
|
||||
},
|
||||
unmounted () {
|
||||
unmounted() {
|
||||
window.removeEventListener('scroll', this.handleScroll)
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
|
||||
if (typeof document.hidden !== 'undefined')
|
||||
document.removeEventListener(
|
||||
'visibilitychange',
|
||||
this.handleVisibilityChange,
|
||||
false,
|
||||
)
|
||||
this.$store.dispatch('clearCurrentChat')
|
||||
},
|
||||
computed: {
|
||||
recipient () {
|
||||
recipient() {
|
||||
return this.currentChat && this.currentChat.account
|
||||
},
|
||||
recipientId () {
|
||||
recipientId() {
|
||||
return this.$route.params.recipient_id
|
||||
},
|
||||
formPlaceholder () {
|
||||
formPlaceholder() {
|
||||
if (this.recipient) {
|
||||
return this.$t('chats.message_user', { nickname: this.recipient.screen_name_ui })
|
||||
return this.$t('chats.message_user', {
|
||||
nickname: this.recipient.screen_name_ui,
|
||||
})
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
chatViewItems () {
|
||||
chatViewItems() {
|
||||
return chatService.getView(this.currentChatMessageService)
|
||||
},
|
||||
newMessageCount () {
|
||||
return this.currentChatMessageService && this.currentChatMessageService.newMessageCount
|
||||
newMessageCount() {
|
||||
return (
|
||||
this.currentChatMessageService &&
|
||||
this.currentChatMessageService.newMessageCount
|
||||
)
|
||||
},
|
||||
streamingEnabled () {
|
||||
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||
streamingEnabled() {
|
||||
return (
|
||||
this.mergedConfig.useStreamingApi &&
|
||||
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||
)
|
||||
},
|
||||
...mapGetters([
|
||||
'currentChat',
|
||||
'currentChatMessageService',
|
||||
'findOpenedChatByRecipientId',
|
||||
'mergedConfig'
|
||||
'mergedConfig',
|
||||
]),
|
||||
...mapPiniaState(useInterfaceStore, {
|
||||
mobileLayout: store => store.layoutType === 'mobile'
|
||||
mobileLayout: (store) => store.layoutType === 'mobile',
|
||||
}),
|
||||
...mapState({
|
||||
backendInteractor: state => state.api.backendInteractor,
|
||||
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus,
|
||||
currentUser: state => state.users.currentUser
|
||||
})
|
||||
backendInteractor: (state) => state.api.backendInteractor,
|
||||
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
}),
|
||||
},
|
||||
watch: {
|
||||
chatViewItems () {
|
||||
chatViewItems() {
|
||||
// We don't want to scroll to the bottom on a new message when the user is viewing older messages.
|
||||
// Therefore we need to know whether the scroll position was at the bottom before the DOM update.
|
||||
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
|
||||
|
|
@ -115,23 +133,23 @@ const Chat = {
|
|||
$route: function () {
|
||||
this.startFetching()
|
||||
},
|
||||
mastoUserSocketStatus (newValue) {
|
||||
mastoUserSocketStatus(newValue) {
|
||||
if (newValue === WSConnectionStatus.JOINED) {
|
||||
this.fetchChat({ isFirstFetch: true })
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// Used to animate the avatar near the first message of the message chain when any message belonging to the chain is hovered
|
||||
onMessageHover ({ isHovered, messageChainId }) {
|
||||
onMessageHover({ isHovered, messageChainId }) {
|
||||
this.hoveredMessageChainId = isHovered ? messageChainId : undefined
|
||||
},
|
||||
onFilesDropped () {
|
||||
onFilesDropped() {
|
||||
this.$nextTick(() => {
|
||||
this.handleResize()
|
||||
})
|
||||
},
|
||||
handleVisibilityChange () {
|
||||
handleVisibilityChange() {
|
||||
this.$nextTick(() => {
|
||||
if (!document.hidden && this.bottomedOut(BOTTOMED_OUT_OFFSET)) {
|
||||
this.scrollDown({ forceRead: true })
|
||||
|
|
@ -139,7 +157,7 @@ const Chat = {
|
|||
})
|
||||
},
|
||||
// "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
|
||||
handleResize (opts = {}) {
|
||||
handleResize(opts = {}) {
|
||||
const { delayed = false } = opts
|
||||
|
||||
if (delayed) {
|
||||
|
|
@ -160,40 +178,56 @@ const Chat = {
|
|||
this.lastScrollPosition = getScrollPosition()
|
||||
})
|
||||
},
|
||||
scrollDown (options = {}) {
|
||||
scrollDown(options = {}) {
|
||||
const { behavior = 'auto', forceRead = false } = options
|
||||
this.$nextTick(() => {
|
||||
window.scrollTo({ top: document.documentElement.scrollHeight, behavior })
|
||||
window.scrollTo({
|
||||
top: document.documentElement.scrollHeight,
|
||||
behavior,
|
||||
})
|
||||
})
|
||||
if (forceRead) {
|
||||
this.readChat()
|
||||
}
|
||||
},
|
||||
readChat () {
|
||||
if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return }
|
||||
if (document.hidden) { return }
|
||||
readChat() {
|
||||
if (
|
||||
!(
|
||||
this.currentChatMessageService && this.currentChatMessageService.maxId
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (document.hidden) {
|
||||
return
|
||||
}
|
||||
const lastReadId = this.currentChatMessageService.maxId
|
||||
this.$store.dispatch('readChat', {
|
||||
id: this.currentChat.id,
|
||||
lastReadId
|
||||
lastReadId,
|
||||
})
|
||||
},
|
||||
bottomedOut (offset) {
|
||||
bottomedOut(offset) {
|
||||
return isBottomedOut(offset)
|
||||
},
|
||||
reachedTop () {
|
||||
reachedTop() {
|
||||
return window.scrollY <= 0
|
||||
},
|
||||
cullOlderCheck () {
|
||||
cullOlderCheck() {
|
||||
window.setTimeout(() => {
|
||||
if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
||||
this.$store.dispatch('cullOlderMessages', this.currentChatMessageService.chatId)
|
||||
this.$store.dispatch(
|
||||
'cullOlderMessages',
|
||||
this.currentChatMessageService.chatId,
|
||||
)
|
||||
}
|
||||
}, 5000)
|
||||
},
|
||||
handleScroll: _.throttle(function () {
|
||||
this.lastScrollPosition = getScrollPosition()
|
||||
if (!this.currentChat) { return }
|
||||
if (!this.currentChat) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.reachedTop()) {
|
||||
this.fetchChat({ maxId: this.currentChatMessageService.minId })
|
||||
|
|
@ -213,22 +247,27 @@ const Chat = {
|
|||
this.jumpToBottomButtonVisible = true
|
||||
}
|
||||
}, 200),
|
||||
handleScrollUp (positionBeforeLoading) {
|
||||
handleScrollUp(positionBeforeLoading) {
|
||||
const positionAfterLoading = getScrollPosition()
|
||||
window.scrollTo({
|
||||
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading)
|
||||
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
|
||||
})
|
||||
},
|
||||
fetchChat ({ isFirstFetch = false, fetchLatest = false, maxId }) {
|
||||
fetchChat({ isFirstFetch = false, fetchLatest = false, maxId }) {
|
||||
const chatMessageService = this.currentChatMessageService
|
||||
if (!chatMessageService) { return }
|
||||
if (fetchLatest && this.streamingEnabled) { return }
|
||||
if (!chatMessageService) {
|
||||
return
|
||||
}
|
||||
if (fetchLatest && this.streamingEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const chatId = chatMessageService.chatId
|
||||
const fetchOlderMessages = !!maxId
|
||||
const sinceId = fetchLatest && chatMessageService.maxId
|
||||
|
||||
return this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId })
|
||||
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) {
|
||||
|
|
@ -236,28 +275,34 @@ const Chat = {
|
|||
}
|
||||
|
||||
const positionBeforeUpdate = getScrollPosition()
|
||||
this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => {
|
||||
this.$nextTick(() => {
|
||||
if (fetchOlderMessages) {
|
||||
this.handleScrollUp(positionBeforeUpdate)
|
||||
}
|
||||
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 () {
|
||||
async startFetching() {
|
||||
let chat = this.findOpenedChatByRecipientId(this.recipientId)
|
||||
if (!chat) {
|
||||
try {
|
||||
chat = await this.backendInteractor.getOrCreateChat({ accountId: this.recipientId })
|
||||
chat = await this.backendInteractor.getOrCreateChat({
|
||||
accountId: this.recipientId,
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Error creating or getting a chat', e)
|
||||
this.errorLoadingChat = true
|
||||
|
|
@ -271,13 +316,14 @@ const Chat = {
|
|||
this.doStartFetching()
|
||||
}
|
||||
},
|
||||
doStartFetching () {
|
||||
doStartFetching() {
|
||||
this.$store.dispatch('startFetchingCurrentChat', {
|
||||
fetcher: () => promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000)
|
||||
fetcher: () =>
|
||||
promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000),
|
||||
})
|
||||
this.fetchChat({ isFirstFetch: true })
|
||||
},
|
||||
handleAttachmentPosting () {
|
||||
handleAttachmentPosting() {
|
||||
this.$nextTick(() => {
|
||||
this.handleResize()
|
||||
// When the posting form size changes because of a media attachment, we need an extra resize
|
||||
|
|
@ -285,11 +331,11 @@ const Chat = {
|
|||
this.scrollDown({ forceRead: true })
|
||||
})
|
||||
},
|
||||
sendMessage ({ status, media, idempotencyKey }) {
|
||||
sendMessage({ status, media, idempotencyKey }) {
|
||||
const params = {
|
||||
id: this.currentChat.id,
|
||||
content: status,
|
||||
idempotencyKey
|
||||
idempotencyKey,
|
||||
}
|
||||
|
||||
if (media[0]) {
|
||||
|
|
@ -301,52 +347,72 @@ const Chat = {
|
|||
chatId: this.currentChat.id,
|
||||
content: status,
|
||||
userId: this.currentUser.id,
|
||||
idempotencyKey
|
||||
idempotencyKey,
|
||||
})
|
||||
|
||||
this.$store.dispatch('addChatMessages', {
|
||||
chatId: this.currentChat.id,
|
||||
messages: [fakeMessage]
|
||||
}).then(() => {
|
||||
this.handleAttachmentPosting()
|
||||
})
|
||||
this.$store
|
||||
.dispatch('addChatMessages', {
|
||||
chatId: this.currentChat.id,
|
||||
messages: [fakeMessage],
|
||||
})
|
||||
.then(() => {
|
||||
this.handleAttachmentPosting()
|
||||
})
|
||||
|
||||
return this.doSendMessage({ params, fakeMessage, retriesLeft: MAX_RETRIES })
|
||||
return this.doSendMessage({
|
||||
params,
|
||||
fakeMessage,
|
||||
retriesLeft: MAX_RETRIES,
|
||||
})
|
||||
},
|
||||
doSendMessage ({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
|
||||
doSendMessage({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
|
||||
if (retriesLeft <= 0) return
|
||||
|
||||
this.backendInteractor.sendChatMessage(params)
|
||||
.then(data => {
|
||||
this.backendInteractor
|
||||
.sendChatMessage(params)
|
||||
.then((data) => {
|
||||
this.$store.dispatch('addChatMessages', {
|
||||
chatId: this.currentChat.id,
|
||||
updateMaxId: false,
|
||||
messages: [{ ...data, fakeId: fakeMessage.id }]
|
||||
messages: [{ ...data, fakeId: fakeMessage.id }],
|
||||
})
|
||||
|
||||
return data
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error('Error sending message', error)
|
||||
this.$store.dispatch('handleMessageError', {
|
||||
chatId: this.currentChat.id,
|
||||
fakeId: fakeMessage.id,
|
||||
isRetry: retriesLeft !== MAX_RETRIES
|
||||
isRetry: retriesLeft !== MAX_RETRIES,
|
||||
})
|
||||
if ((error.statusCode >= 500 && error.statusCode < 600) || error.message === 'Failed to fetch') {
|
||||
this.messageRetriers[fakeMessage.id] = setTimeout(() => {
|
||||
this.doSendMessage({ params, fakeMessage, retriesLeft: retriesLeft - 1 })
|
||||
}, 1000 * (2 ** (MAX_RETRIES - retriesLeft)))
|
||||
if (
|
||||
(error.statusCode >= 500 && error.statusCode < 600) ||
|
||||
error.message === 'Failed to fetch'
|
||||
) {
|
||||
this.messageRetriers[fakeMessage.id] = setTimeout(
|
||||
() => {
|
||||
this.doSendMessage({
|
||||
params,
|
||||
fakeMessage,
|
||||
retriesLeft: retriesLeft - 1,
|
||||
})
|
||||
},
|
||||
1000 * 2 ** (MAX_RETRIES - retriesLeft),
|
||||
)
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
return Promise.resolve(fakeMessage)
|
||||
},
|
||||
goBack () {
|
||||
this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } })
|
||||
}
|
||||
}
|
||||
goBack() {
|
||||
this.$router.push({
|
||||
name: 'chats',
|
||||
params: { username: this.currentUser.screen_name },
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default Chat
|
||||
|
|
|
|||
|
|
@ -1,19 +1,13 @@
|
|||
export default {
|
||||
name: 'Chat',
|
||||
selector: '.chat-message-list',
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Link',
|
||||
'Icon',
|
||||
'Avatar',
|
||||
'ChatMessage'
|
||||
],
|
||||
validInnerComponents: ['Text', 'Link', 'Icon', 'Avatar', 'ChatMessage'],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
background: '--bg',
|
||||
blur: '5px'
|
||||
}
|
||||
}
|
||||
]
|
||||
blur: '5px',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,17 @@ export const getScrollPosition = () => {
|
|||
return {
|
||||
scrollTop: window.scrollY,
|
||||
scrollHeight: document.documentElement.scrollHeight,
|
||||
offsetHeight: window.innerHeight
|
||||
offsetHeight: window.innerHeight,
|
||||
}
|
||||
}
|
||||
|
||||
// A helper function that is used to keep the scroll position fixed as the new elements are added to the top
|
||||
// Takes two scroll positions, before and after the update.
|
||||
export const getNewTopPosition = (previousPosition, newPosition) => {
|
||||
return previousPosition.scrollTop + (newPosition.scrollHeight - previousPosition.scrollHeight)
|
||||
return (
|
||||
previousPosition.scrollTop +
|
||||
(newPosition.scrollHeight - previousPosition.scrollHeight)
|
||||
)
|
||||
}
|
||||
|
||||
export const isBottomedOut = (offset = 0) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { mapState, mapGetters } from 'vuex'
|
||||
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'
|
||||
|
|
@ -7,31 +8,31 @@ const ChatList = {
|
|||
components: {
|
||||
ChatListItem,
|
||||
List,
|
||||
ChatNew
|
||||
ChatNew,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
}),
|
||||
...mapGetters(['sortedChatList'])
|
||||
...mapGetters(['sortedChatList']),
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
isNew: false
|
||||
isNew: false,
|
||||
}
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
this.$store.dispatch('fetchChats', { latest: true })
|
||||
},
|
||||
methods: {
|
||||
cancelNewChat () {
|
||||
cancelNewChat() {
|
||||
this.isNew = false
|
||||
this.$store.dispatch('fetchChats', { latest: true })
|
||||
},
|
||||
newChat () {
|
||||
newChat() {
|
||||
this.isNew = true
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default ChatList
|
||||
|
|
|
|||
|
|
@ -1,31 +1,34 @@
|
|||
import { mapState } from 'vuex'
|
||||
import StatusBody from '../status_content/status_content.vue'
|
||||
|
||||
import fileType from 'src/services/file_type/file_type.service'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import AvatarList from '../avatar_list/avatar_list.vue'
|
||||
import Timeago from '../timeago/timeago.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'
|
||||
|
||||
const ChatListItem = {
|
||||
name: 'ChatListItem',
|
||||
props: [
|
||||
'chat'
|
||||
],
|
||||
props: ['chat'],
|
||||
components: {
|
||||
UserAvatar,
|
||||
AvatarList,
|
||||
Timeago,
|
||||
ChatTitle,
|
||||
StatusBody
|
||||
StatusBody,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
}),
|
||||
attachmentInfo () {
|
||||
if (this.chat.lastMessage.attachments.length === 0) { return }
|
||||
attachmentInfo() {
|
||||
if (this.chat.lastMessage.attachments.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const types = this.chat.lastMessage.attachments.map(file => fileType.fileType(file.mimetype))
|
||||
const types = this.chat.lastMessage.attachments.map((file) =>
|
||||
fileType.fileType(file.mimetype),
|
||||
)
|
||||
if (types.includes('video')) {
|
||||
return this.$t('file_type.video')
|
||||
} else if (types.includes('audio')) {
|
||||
|
|
@ -36,34 +39,36 @@ const ChatListItem = {
|
|||
return this.$t('file_type.file')
|
||||
}
|
||||
},
|
||||
messageForStatusContent () {
|
||||
messageForStatusContent() {
|
||||
const message = this.chat.lastMessage
|
||||
const messageEmojis = message ? message.emojis : []
|
||||
const isYou = message && message.account_id === this.currentUser.id
|
||||
const content = message ? (this.attachmentInfo || message.content) : ''
|
||||
const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content
|
||||
const content = message ? this.attachmentInfo || message.content : ''
|
||||
const messagePreview = isYou
|
||||
? `<i>${this.$t('chats.you')}</i> ${content}`
|
||||
: content
|
||||
return {
|
||||
summary: '',
|
||||
emojis: messageEmojis,
|
||||
raw_html: messagePreview,
|
||||
text: messagePreview,
|
||||
attachments: []
|
||||
attachments: [],
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openChat () {
|
||||
openChat() {
|
||||
if (this.chat.id) {
|
||||
this.$router.push({
|
||||
name: 'chat',
|
||||
params: {
|
||||
username: this.currentUser.screen_name,
|
||||
recipient_id: this.chat.account.id
|
||||
}
|
||||
recipient_id: this.chat.account.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default ChatListItem
|
||||
|
|
|
|||
|
|
@ -1,24 +1,20 @@
|
|||
import { mapState, mapGetters } from 'vuex'
|
||||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import Attachment from '../attachment/attachment.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.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 ChatMessageDate from '../chat_message_date/chat_message_date.vue'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes,
|
||||
faEllipsisH
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
|
||||
library.add(
|
||||
faTimes,
|
||||
faEllipsisH
|
||||
)
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faEllipsisH, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faTimes, faEllipsisH)
|
||||
|
||||
const ChatMessage = {
|
||||
name: 'ChatMessage',
|
||||
|
|
@ -27,7 +23,7 @@ const ChatMessage = {
|
|||
'edited',
|
||||
'noHeading',
|
||||
'chatViewItem',
|
||||
'hoveredMessageChain'
|
||||
'hoveredMessageChain',
|
||||
],
|
||||
emits: ['hover'],
|
||||
components: {
|
||||
|
|
@ -38,73 +34,82 @@ const ChatMessage = {
|
|||
Gallery,
|
||||
LinkPreview,
|
||||
ChatMessageDate,
|
||||
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
|
||||
UserPopover: defineAsyncComponent(
|
||||
() => import('../user_popover/user_popover.vue'),
|
||||
),
|
||||
},
|
||||
computed: {
|
||||
// Returns HH:MM (hours and minutes) in local time.
|
||||
createdAt () {
|
||||
createdAt() {
|
||||
const time = this.chatViewItem.data.created_at
|
||||
return time.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false })
|
||||
return time.toLocaleTimeString('en', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
},
|
||||
isCurrentUser () {
|
||||
isCurrentUser() {
|
||||
return this.message.account_id === this.currentUser.id
|
||||
},
|
||||
message () {
|
||||
message() {
|
||||
return this.chatViewItem.data
|
||||
},
|
||||
isMessage () {
|
||||
isMessage() {
|
||||
return this.chatViewItem.type === 'message'
|
||||
},
|
||||
messageForStatusContent () {
|
||||
messageForStatusContent() {
|
||||
return {
|
||||
summary: '',
|
||||
emojis: this.message.emojis,
|
||||
raw_html: this.message.content || '',
|
||||
text: this.message.content || '',
|
||||
attachments: this.message.attachments
|
||||
attachments: this.message.attachments,
|
||||
}
|
||||
},
|
||||
hasAttachment () {
|
||||
hasAttachment() {
|
||||
return this.message.attachments.length > 0
|
||||
},
|
||||
...mapPiniaState(useInterfaceStore, {
|
||||
betterShadow: store => store.browserSupport.cssFilter
|
||||
betterShadow: (store) => store.browserSupport.cssFilter,
|
||||
}),
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser,
|
||||
restrictedNicknames: state => state.instance.restrictedNicknames
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
restrictedNicknames: (state) => state.instance.restrictedNicknames,
|
||||
}),
|
||||
popoverMarginStyle () {
|
||||
popoverMarginStyle() {
|
||||
if (this.isCurrentUser) {
|
||||
return {}
|
||||
} else {
|
||||
return { left: 50 }
|
||||
}
|
||||
},
|
||||
...mapGetters(['mergedConfig', 'findUser'])
|
||||
...mapGetters(['mergedConfig', 'findUser']),
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
hovered: false,
|
||||
menuOpened: false
|
||||
menuOpened: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onHover (bool) {
|
||||
this.$emit('hover', { isHovered: bool, messageChainId: this.chatViewItem.messageChainId })
|
||||
onHover(bool) {
|
||||
this.$emit('hover', {
|
||||
isHovered: bool,
|
||||
messageChainId: this.chatViewItem.messageChainId,
|
||||
})
|
||||
},
|
||||
async deleteMessage () {
|
||||
async deleteMessage() {
|
||||
const confirmed = window.confirm(this.$t('chats.delete_confirm'))
|
||||
if (confirmed) {
|
||||
await this.$store.dispatch('deleteChatMessage', {
|
||||
messageId: this.chatViewItem.data.id,
|
||||
chatId: this.chatViewItem.data.chat_id
|
||||
chatId: this.chatViewItem.data.chat_id,
|
||||
})
|
||||
}
|
||||
this.hovered = false
|
||||
this.menuOpened = false
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default ChatMessage
|
||||
|
|
|
|||
|
|
@ -2,26 +2,21 @@ export default {
|
|||
name: 'ChatMessage',
|
||||
selector: '.chat-message',
|
||||
variants: {
|
||||
outgoing: '.outgoing'
|
||||
outgoing: '.outgoing',
|
||||
},
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon',
|
||||
'Border',
|
||||
'PollGraph'
|
||||
],
|
||||
validInnerComponents: ['Text', 'Icon', 'Border', 'PollGraph'],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
background: '--bg, 2',
|
||||
backgroundNoCssColor: 'yes'
|
||||
}
|
||||
backgroundNoCssColor: 'yes',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'outgoing',
|
||||
directives: {
|
||||
background: '--bg, 5'
|
||||
}
|
||||
}
|
||||
]
|
||||
background: '--bg, 5',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,16 +11,19 @@ export default {
|
|||
name: 'Timeago',
|
||||
props: ['date'],
|
||||
computed: {
|
||||
displayDate () {
|
||||
displayDate() {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
if (this.date.getTime() === today.getTime()) {
|
||||
return this.$t('display_date.today')
|
||||
} else {
|
||||
return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' })
|
||||
return this.date.toLocaleDateString(
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
{ day: 'numeric', month: 'long' },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,39 +1,35 @@
|
|||
import { mapState, mapGetters } from 'vuex'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faSearch,
|
||||
faChevronLeft
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faSearch,
|
||||
faChevronLeft
|
||||
)
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faSearch, faChevronLeft)
|
||||
|
||||
const chatNew = {
|
||||
components: {
|
||||
BasicUserCard,
|
||||
UserAvatar
|
||||
UserAvatar,
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
suggestions: [],
|
||||
userIds: [],
|
||||
loading: false,
|
||||
query: ''
|
||||
query: '',
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
async created() {
|
||||
const { chats } = await this.backendInteractor.chats()
|
||||
chats.forEach(chat => this.suggestions.push(chat.account))
|
||||
chats.forEach((chat) => this.suggestions.push(chat.account))
|
||||
},
|
||||
computed: {
|
||||
users () {
|
||||
return this.userIds.map(userId => this.findUser(userId))
|
||||
users() {
|
||||
return this.userIds.map((userId) => this.findUser(userId))
|
||||
},
|
||||
availableUsers () {
|
||||
availableUsers() {
|
||||
if (this.query.length !== 0) {
|
||||
return this.users
|
||||
} else {
|
||||
|
|
@ -41,29 +37,29 @@ const chatNew = {
|
|||
}
|
||||
},
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser,
|
||||
backendInteractor: state => state.api.backendInteractor
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
backendInteractor: (state) => state.api.backendInteractor,
|
||||
}),
|
||||
...mapGetters(['findUser'])
|
||||
...mapGetters(['findUser']),
|
||||
},
|
||||
methods: {
|
||||
goBack () {
|
||||
goBack() {
|
||||
this.$emit('cancel')
|
||||
},
|
||||
goToChat (user) {
|
||||
goToChat(user) {
|
||||
this.$router.push({ name: 'chat', params: { recipient_id: user.id } })
|
||||
},
|
||||
onInput () {
|
||||
onInput() {
|
||||
this.search(this.query)
|
||||
},
|
||||
addUser (user) {
|
||||
addUser(user) {
|
||||
this.selectedUserIds.push(user.id)
|
||||
this.query = ''
|
||||
},
|
||||
removeUser (userId) {
|
||||
this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId)
|
||||
removeUser(userId) {
|
||||
this.selectedUserIds = this.selectedUserIds.filter((id) => id !== userId)
|
||||
},
|
||||
search (query) {
|
||||
search(query) {
|
||||
if (!query) {
|
||||
this.loading = false
|
||||
return
|
||||
|
|
@ -71,13 +67,14 @@ const chatNew = {
|
|||
|
||||
this.loading = true
|
||||
this.userIds = []
|
||||
this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts' })
|
||||
.then(data => {
|
||||
this.$store
|
||||
.dispatch('search', { q: query, resolve: true, type: 'accounts' })
|
||||
.then((data) => {
|
||||
this.loading = false
|
||||
this.userIds = data.accounts.map(a => a.id)
|
||||
this.userIds = data.accounts.map((a) => a.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default chatNew
|
||||
|
|
|
|||
|
|
@ -1,23 +1,24 @@
|
|||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
|
||||
export default {
|
||||
name: 'ChatTitle',
|
||||
components: {
|
||||
UserAvatar,
|
||||
RichContent,
|
||||
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
|
||||
UserPopover: defineAsyncComponent(
|
||||
() => import('../user_popover/user_popover.vue'),
|
||||
),
|
||||
},
|
||||
props: [
|
||||
'user', 'withAvatar'
|
||||
],
|
||||
props: ['user', 'withAvatar'],
|
||||
computed: {
|
||||
title () {
|
||||
title() {
|
||||
return this.user ? this.user.screen_name_ui : ''
|
||||
},
|
||||
htmlTitle () {
|
||||
htmlTitle() {
|
||||
return this.user ? this.user.name_html : ''
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,30 +36,25 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
'radio',
|
||||
'modelValue',
|
||||
'indeterminate',
|
||||
'disabled'
|
||||
],
|
||||
props: ['radio', 'modelValue', 'indeterminate', 'disabled'],
|
||||
emits: ['update:modelValue'],
|
||||
data: (vm) => ({
|
||||
indeterminateTransitionFix: vm.indeterminate
|
||||
indeterminateTransitionFix: vm.indeterminate,
|
||||
}),
|
||||
watch: {
|
||||
indeterminate (e) {
|
||||
indeterminate(e) {
|
||||
if (e) {
|
||||
this.indeterminateTransitionFix = true
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onTransitionEnd () {
|
||||
onTransitionEnd() {
|
||||
if (!this.indeterminate) {
|
||||
this.indeterminateTransitionFix = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -64,95 +64,95 @@
|
|||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
import { throttle } from 'lodash'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faEyeDropper
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
|
||||
library.add(
|
||||
faEyeDropper
|
||||
)
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faEyeDropper } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faEyeDropper)
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Checkbox
|
||||
Checkbox,
|
||||
},
|
||||
props: {
|
||||
// Name of color, used for identifying
|
||||
name: {
|
||||
required: true,
|
||||
type: String
|
||||
type: String,
|
||||
},
|
||||
// Readable label
|
||||
label: {
|
||||
required: true,
|
||||
type: String
|
||||
type: String,
|
||||
},
|
||||
// use unstyled, uh, style
|
||||
unstyled: {
|
||||
required: false,
|
||||
type: Boolean
|
||||
type: Boolean,
|
||||
},
|
||||
// Color value, should be required but vue cannot tell the difference
|
||||
// between "property missing" and "property set to undefined"
|
||||
modelValue: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: undefined
|
||||
default: undefined,
|
||||
},
|
||||
// Color fallback to use when value is not defeind
|
||||
fallback: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: undefined
|
||||
default: undefined,
|
||||
},
|
||||
// Disable the control
|
||||
disabled: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
// Show "optional" tickbox, for when value might become mandatory
|
||||
showOptionalCheckbox: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: true
|
||||
default: true,
|
||||
},
|
||||
// Force "optional" tickbox to hide
|
||||
hideOptionalCheckbox: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
compact: {
|
||||
required: false,
|
||||
type: Boolean
|
||||
}
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
computed: {
|
||||
present () {
|
||||
present() {
|
||||
return typeof this.modelValue !== 'undefined'
|
||||
},
|
||||
validColor () {
|
||||
validColor() {
|
||||
return hex2rgb(this.modelValue || this.fallback)
|
||||
},
|
||||
transparentColor () {
|
||||
transparentColor() {
|
||||
return this.modelValue === 'transparent'
|
||||
},
|
||||
computedColor () {
|
||||
return this.modelValue && (this.modelValue.startsWith('--') || this.modelValue.startsWith('$'))
|
||||
}
|
||||
computedColor() {
|
||||
return (
|
||||
this.modelValue &&
|
||||
(this.modelValue.startsWith('--') || this.modelValue.startsWith('$'))
|
||||
)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateValue: throttle(function (value) {
|
||||
this.$emit('update:modelValue', value)
|
||||
}, 100)
|
||||
}
|
||||
}, 100),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" src="./color_input.scss"></style>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||
|
||||
import genRandomSeed from 'src/services/random_seed/random_seed.service.js'
|
||||
import { createStyleSheet, adoptStyleSheets } from 'src/services/style_setter/style_setter.js'
|
||||
import {
|
||||
adoptStyleSheets,
|
||||
createStyleSheet,
|
||||
} from 'src/services/style_setter/style_setter.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Checkbox,
|
||||
ColorInput
|
||||
ColorInput,
|
||||
},
|
||||
props: [
|
||||
'shadow',
|
||||
|
|
@ -17,41 +19,41 @@ export default {
|
|||
'previewCss',
|
||||
'disabled',
|
||||
'invalid',
|
||||
'noColorControl'
|
||||
'noColorControl',
|
||||
],
|
||||
emits: ['update:shadow'],
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
colorOverride: undefined,
|
||||
lightGrid: false,
|
||||
zoom: 100,
|
||||
randomSeed: genRandomSeed()
|
||||
randomSeed: genRandomSeed(),
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
this.update()
|
||||
},
|
||||
computed: {
|
||||
hideControls () {
|
||||
hideControls() {
|
||||
return typeof this.shadow === 'string'
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
previewCss () {
|
||||
previewCss() {
|
||||
this.update()
|
||||
},
|
||||
previewStyle () {
|
||||
previewStyle() {
|
||||
this.update()
|
||||
},
|
||||
zoom () {
|
||||
zoom() {
|
||||
this.update()
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateProperty (axis, value) {
|
||||
updateProperty(axis, value) {
|
||||
this.$emit('update:shadow', { axis, value: Number(value) })
|
||||
},
|
||||
update () {
|
||||
update() {
|
||||
const sheet = createStyleSheet('style-component-preview', 90)
|
||||
|
||||
sheet.clear()
|
||||
|
|
@ -60,23 +62,25 @@ export default {
|
|||
if (this.colorOverride) result.push(`--background: ${this.colorOverride}`)
|
||||
|
||||
const styleRule = [
|
||||
'#component-preview-', this.randomSeed, ' {\n',
|
||||
'#component-preview-',
|
||||
this.randomSeed,
|
||||
' {\n',
|
||||
'.preview-block {\n',
|
||||
`zoom: ${this.zoom / 100};`,
|
||||
this.previewStyle,
|
||||
'\n}',
|
||||
'\n}'
|
||||
'\n}',
|
||||
].join('')
|
||||
|
||||
sheet.addRule(styleRule)
|
||||
sheet.addRule([
|
||||
'#component-preview-', this.randomSeed, ' {\n',
|
||||
...result,
|
||||
'\n}'
|
||||
].join(''))
|
||||
sheet.addRule(
|
||||
['#component-preview-', this.randomSeed, ' {\n', ...result, '\n}'].join(
|
||||
'',
|
||||
),
|
||||
)
|
||||
|
||||
sheet.ready = true
|
||||
adoptStyleSheets()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,30 +9,29 @@ import DialogModal from '../dialog_modal/dialog_modal.vue'
|
|||
*/
|
||||
const ConfirmModal = {
|
||||
components: {
|
||||
DialogModal
|
||||
DialogModal,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String
|
||||
type: String,
|
||||
},
|
||||
cancelText: {
|
||||
type: String
|
||||
type: String,
|
||||
},
|
||||
confirmText: {
|
||||
type: String
|
||||
}
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
emits: ['cancelled', 'accepted'],
|
||||
computed: {
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
onCancel () {
|
||||
onCancel() {
|
||||
this.$emit('cancelled')
|
||||
},
|
||||
onAccept () {
|
||||
onAccept() {
|
||||
this.$emit('accepted')
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default ConfirmModal
|
||||
|
|
|
|||
|
|
@ -1,64 +1,67 @@
|
|||
import { mapGetters } from 'vuex'
|
||||
|
||||
import ConfirmModal from './confirm_modal.vue'
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import ConfirmModal from './confirm_modal.vue'
|
||||
|
||||
export default {
|
||||
props: ['type', 'user', 'status'],
|
||||
emits: ['hide', 'show', 'muted'],
|
||||
data: () => ({
|
||||
showing: false
|
||||
showing: false,
|
||||
}),
|
||||
components: {
|
||||
ConfirmModal,
|
||||
Select
|
||||
Select,
|
||||
},
|
||||
computed: {
|
||||
domain () {
|
||||
domain() {
|
||||
return this.user.fqn.split('@')[1]
|
||||
},
|
||||
keypath () {
|
||||
keypath() {
|
||||
if (this.type === 'domain') {
|
||||
return 'user_card.mute_domain_confirm'
|
||||
} else if (this.type === 'conversation') {
|
||||
return 'user_card.mute_conversation_confirm'
|
||||
}
|
||||
},
|
||||
conversationIsMuted () {
|
||||
conversationIsMuted() {
|
||||
return this.status.conversation_muted
|
||||
},
|
||||
domainIsMuted () {
|
||||
return new Set(this.$store.state.users.currentUser.domainMutes).has(this.domain)
|
||||
domainIsMuted() {
|
||||
return new Set(this.$store.state.users.currentUser.domainMutes).has(
|
||||
this.domain,
|
||||
)
|
||||
},
|
||||
shouldConfirm () {
|
||||
shouldConfirm() {
|
||||
switch (this.type) {
|
||||
case 'domain': {
|
||||
return this.mergedConfig.modalOnMuteDomain
|
||||
}
|
||||
default: { // conversation
|
||||
default: {
|
||||
// conversation
|
||||
return this.mergedConfig.modalOnMuteConversation
|
||||
}
|
||||
}
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
...mapGetters(['mergedConfig']),
|
||||
},
|
||||
methods: {
|
||||
optionallyPrompt () {
|
||||
optionallyPrompt() {
|
||||
if (this.shouldConfirm) {
|
||||
this.show()
|
||||
} else {
|
||||
this.doMute()
|
||||
}
|
||||
},
|
||||
show () {
|
||||
show() {
|
||||
this.showing = true
|
||||
this.$emit('show')
|
||||
},
|
||||
hide () {
|
||||
hide() {
|
||||
this.showing = false
|
||||
this.$emit('hide')
|
||||
},
|
||||
doMute () {
|
||||
doMute() {
|
||||
switch (this.type) {
|
||||
case 'domain': {
|
||||
if (!this.domainIsMuted) {
|
||||
|
|
@ -79,6 +82,6 @@ export default {
|
|||
}
|
||||
this.$emit('muted')
|
||||
this.hide()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,54 +63,68 @@ import { library } from '@fortawesome/fontawesome-svg-core'
|
|||
import {
|
||||
faAdjust,
|
||||
faExclamationTriangle,
|
||||
faThumbsUp
|
||||
faThumbsUp,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faAdjust,
|
||||
faExclamationTriangle,
|
||||
faThumbsUp
|
||||
)
|
||||
library.add(faAdjust, faExclamationTriangle, faThumbsUp)
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tooltip
|
||||
Tooltip,
|
||||
},
|
||||
props: {
|
||||
large: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
// TODO: Make theme switcher compute theme initially so that contrast
|
||||
// component won't be called without contrast data
|
||||
contrast: {
|
||||
required: false,
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
default: () => ({
|
||||
/* no-op */
|
||||
}),
|
||||
},
|
||||
showRatio: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hint () {
|
||||
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
|
||||
hint() {
|
||||
const levelVal = this.contrast.aaa
|
||||
? 'aaa'
|
||||
: this.contrast.aa
|
||||
? 'aa'
|
||||
: 'bad'
|
||||
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
|
||||
const context = this.$t('settings.style.common.contrast.context.text')
|
||||
const ratio = this.contrast.text
|
||||
return this.$t('settings.style.common.contrast.hint', { level, context, ratio })
|
||||
return this.$t('settings.style.common.contrast.hint', {
|
||||
level,
|
||||
context,
|
||||
ratio,
|
||||
})
|
||||
},
|
||||
hint_18pt () {
|
||||
const levelVal = this.contrast.laaa ? 'aaa' : (this.contrast.laa ? 'aa' : 'bad')
|
||||
hint_18pt() {
|
||||
const levelVal = this.contrast.laaa
|
||||
? 'aaa'
|
||||
: this.contrast.laa
|
||||
? 'aa'
|
||||
: 'bad'
|
||||
const level = this.$t(`settings.style.common.contrast.level.${levelVal}`)
|
||||
const context = this.$t('settings.style.common.contrast.context.18pt')
|
||||
const ratio = this.contrast.text
|
||||
return this.$t('settings.style.common.contrast.hint', { level, context, ratio })
|
||||
}
|
||||
}
|
||||
return this.$t('settings.style.common.contrast.hint', {
|
||||
level,
|
||||
context,
|
||||
ratio,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ import Conversation from '../conversation/conversation.vue'
|
|||
|
||||
const conversationPage = {
|
||||
components: {
|
||||
Conversation
|
||||
Conversation,
|
||||
},
|
||||
computed: {
|
||||
statusId () {
|
||||
statusId() {
|
||||
return this.$route.params.id
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default conversationPage
|
||||
|
|
|
|||
|
|
@ -1,25 +1,22 @@
|
|||
import { reduce, filter, findIndex, clone, get } from 'lodash'
|
||||
import Status from '../status/status.vue'
|
||||
import ThreadTree from '../thread_tree/thread_tree.vue'
|
||||
import { WSConnectionStatus } from '../../services/api/api.service.js'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { clone, filter, findIndex, get, reduce } from 'lodash'
|
||||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
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 { useInterfaceStore } from 'src/stores/interface'
|
||||
import Status from '../status/status.vue'
|
||||
import ThreadTree from '../thread_tree/thread_tree.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faAngleDoubleDown,
|
||||
faAngleDoubleLeft,
|
||||
faChevronLeft
|
||||
faChevronLeft,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faAngleDoubleDown,
|
||||
faAngleDoubleLeft,
|
||||
faChevronLeft
|
||||
)
|
||||
library.add(faAngleDoubleDown, faAngleDoubleLeft, faChevronLeft)
|
||||
|
||||
const sortById = (a, b) => {
|
||||
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
||||
|
|
@ -43,23 +40,25 @@ const sortAndFilterConversation = (conversation, statusoid) => {
|
|||
if (statusoid.type === 'retweet') {
|
||||
conversation = filter(
|
||||
conversation,
|
||||
(status) => (status.type === 'retweet' || status.id !== statusoid.retweeted_status.id)
|
||||
(status) =>
|
||||
status.type === 'retweet' ||
|
||||
status.id !== statusoid.retweeted_status.id,
|
||||
)
|
||||
} else {
|
||||
conversation = filter(conversation, (status) => status.type !== 'retweet')
|
||||
}
|
||||
return conversation.filter(_ => _).sort(sortById)
|
||||
return conversation.filter((_) => _).sort(sortById)
|
||||
}
|
||||
|
||||
const conversation = {
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
highlight: null,
|
||||
expanded: false,
|
||||
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
|
||||
statusContentPropertiesObject: {},
|
||||
inlineDivePosition: null,
|
||||
loadStatusError: null
|
||||
loadStatusError: null,
|
||||
}
|
||||
},
|
||||
props: [
|
||||
|
|
@ -69,76 +68,80 @@ const conversation = {
|
|||
'pinnedStatusIdsObject',
|
||||
'inProfile',
|
||||
'profileUserId',
|
||||
'virtualHidden'
|
||||
'virtualHidden',
|
||||
],
|
||||
created () {
|
||||
created() {
|
||||
if (this.isPage) {
|
||||
this.fetchConversation()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
maxDepthToShowByDefault () {
|
||||
maxDepthToShowByDefault() {
|
||||
// maxDepthInThread = max number of depths that is *visible*
|
||||
// since our depth starts with 0 and "showing" means "showing children"
|
||||
// there is a -2 here
|
||||
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
|
||||
return maxDepth >= 1 ? maxDepth : 1
|
||||
},
|
||||
streamingEnabled () {
|
||||
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||
streamingEnabled() {
|
||||
return (
|
||||
this.mergedConfig.useStreamingApi &&
|
||||
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||
)
|
||||
},
|
||||
displayStyle () {
|
||||
displayStyle() {
|
||||
return this.$store.getters.mergedConfig.conversationDisplay
|
||||
},
|
||||
isTreeView () {
|
||||
isTreeView() {
|
||||
return !this.isLinearView
|
||||
},
|
||||
treeViewIsSimple () {
|
||||
treeViewIsSimple() {
|
||||
return !this.$store.getters.mergedConfig.conversationTreeAdvanced
|
||||
},
|
||||
isLinearView () {
|
||||
isLinearView() {
|
||||
return this.displayStyle === 'linear'
|
||||
},
|
||||
shouldFadeAncestors () {
|
||||
shouldFadeAncestors() {
|
||||
return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
|
||||
},
|
||||
otherRepliesButtonPosition () {
|
||||
otherRepliesButtonPosition() {
|
||||
return this.$store.getters.mergedConfig.conversationOtherRepliesButton
|
||||
},
|
||||
showOtherRepliesButtonBelowStatus () {
|
||||
showOtherRepliesButtonBelowStatus() {
|
||||
return this.otherRepliesButtonPosition === 'below'
|
||||
},
|
||||
showOtherRepliesButtonInsideStatus () {
|
||||
showOtherRepliesButtonInsideStatus() {
|
||||
return this.otherRepliesButtonPosition === 'inside'
|
||||
},
|
||||
suspendable () {
|
||||
suspendable() {
|
||||
if (this.isTreeView) {
|
||||
return Object.entries(this.statusContentProperties)
|
||||
.every(([, prop]) => !prop.replying && prop.mediaPlaying.length === 0)
|
||||
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)
|
||||
return this.$refs.statusComponent.every((s) => s.suspendable)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
},
|
||||
hideStatus () {
|
||||
hideStatus() {
|
||||
return this.virtualHidden && this.suspendable
|
||||
},
|
||||
status () {
|
||||
status() {
|
||||
return this.$store.state.statuses.allStatusesObject[this.statusId]
|
||||
},
|
||||
originalStatusId () {
|
||||
originalStatusId() {
|
||||
if (this.status.retweeted_status) {
|
||||
return this.status.retweeted_status.id
|
||||
} else {
|
||||
return this.statusId
|
||||
}
|
||||
},
|
||||
conversationId () {
|
||||
conversationId() {
|
||||
return this.getConversationId(this.statusId)
|
||||
},
|
||||
conversation () {
|
||||
conversation() {
|
||||
if (!this.status) {
|
||||
return []
|
||||
}
|
||||
|
|
@ -147,7 +150,9 @@ const conversation = {
|
|||
return [this.status]
|
||||
}
|
||||
|
||||
const conversation = clone(this.$store.state.statuses.conversationsObject[this.conversationId])
|
||||
const conversation = clone(
|
||||
this.$store.state.statuses.conversationsObject[this.conversationId],
|
||||
)
|
||||
const statusIndex = findIndex(conversation, { id: this.originalStatusId })
|
||||
if (statusIndex !== -1) {
|
||||
conversation[statusIndex] = this.status
|
||||
|
|
@ -155,144 +160,188 @@ const conversation = {
|
|||
|
||||
return sortAndFilterConversation(conversation, this.status)
|
||||
},
|
||||
statusMap () {
|
||||
statusMap() {
|
||||
return this.conversation.reduce((res, s) => {
|
||||
res[s.id] = s
|
||||
return res
|
||||
}, {})
|
||||
},
|
||||
threadTree () {
|
||||
const reverseLookupTable = this.conversation.reduce((table, status, index) => {
|
||||
table[status.id] = index
|
||||
return table
|
||||
}, {})
|
||||
threadTree() {
|
||||
const reverseLookupTable = this.conversation.reduce(
|
||||
(table, status, index) => {
|
||||
table[status.id] = index
|
||||
return table
|
||||
},
|
||||
{},
|
||||
)
|
||||
|
||||
const threads = this.conversation.reduce((a, cur) => {
|
||||
const id = cur.id
|
||||
a.forest[id] = this.getReplies(id)
|
||||
.map(s => s.id)
|
||||
const threads = this.conversation.reduce(
|
||||
(a, cur) => {
|
||||
const id = cur.id
|
||||
a.forest[id] = this.getReplies(id).map((s) => s.id)
|
||||
|
||||
return a
|
||||
}, {
|
||||
forest: {}
|
||||
})
|
||||
return a
|
||||
},
|
||||
{
|
||||
forest: {},
|
||||
},
|
||||
)
|
||||
|
||||
const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
|
||||
if (processed[id]) {
|
||||
return []
|
||||
}
|
||||
const walk = (forest, topLevel, depth = 0, processed = {}) =>
|
||||
topLevel
|
||||
.map((id) => {
|
||||
if (processed[id]) {
|
||||
return []
|
||||
}
|
||||
|
||||
processed[id] = true
|
||||
return [{
|
||||
status: this.conversation[reverseLookupTable[id]],
|
||||
id,
|
||||
depth
|
||||
}, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), [])
|
||||
}).reduce((a, b) => a.concat(b), [])
|
||||
processed[id] = true
|
||||
return [
|
||||
{
|
||||
status: this.conversation[reverseLookupTable[id]],
|
||||
id,
|
||||
depth,
|
||||
},
|
||||
walk(forest, forest[id], depth + 1, processed),
|
||||
].reduce((a, b) => a.concat(b), [])
|
||||
})
|
||||
.reduce((a, b) => a.concat(b), [])
|
||||
|
||||
const linearized = walk(threads.forest, this.topLevel.map(k => k.id))
|
||||
const linearized = walk(
|
||||
threads.forest,
|
||||
this.topLevel.map((k) => k.id),
|
||||
)
|
||||
|
||||
return linearized
|
||||
},
|
||||
replyIds () {
|
||||
return this.conversation.map(k => k.id)
|
||||
replyIds() {
|
||||
return this.conversation
|
||||
.map((k) => k.id)
|
||||
.reduce((res, id) => {
|
||||
res[id] = (this.replies[id] || []).map(k => k.id)
|
||||
res[id] = (this.replies[id] || []).map((k) => k.id)
|
||||
return res
|
||||
}, {})
|
||||
},
|
||||
totalReplyCount () {
|
||||
totalReplyCount() {
|
||||
const sizes = {}
|
||||
const subTreeSizeFor = (id) => {
|
||||
if (sizes[id]) {
|
||||
return sizes[id]
|
||||
}
|
||||
sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0)
|
||||
sizes[id] =
|
||||
1 +
|
||||
this.replyIds[id]
|
||||
.map((cid) => subTreeSizeFor(cid))
|
||||
.reduce((a, b) => a + b, 0)
|
||||
return sizes[id]
|
||||
}
|
||||
this.conversation.map(k => k.id).map(subTreeSizeFor)
|
||||
this.conversation.map((k) => k.id).map(subTreeSizeFor)
|
||||
return Object.keys(sizes).reduce((res, id) => {
|
||||
res[id] = sizes[id] - 1 // exclude itself
|
||||
return res
|
||||
}, {})
|
||||
},
|
||||
totalReplyDepth () {
|
||||
totalReplyDepth() {
|
||||
const depths = {}
|
||||
const subTreeDepthFor = (id) => {
|
||||
if (depths[id]) {
|
||||
return depths[id]
|
||||
}
|
||||
depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0)
|
||||
depths[id] =
|
||||
1 +
|
||||
this.replyIds[id]
|
||||
.map((cid) => subTreeDepthFor(cid))
|
||||
.reduce((a, b) => (a > b ? a : b), 0)
|
||||
return depths[id]
|
||||
}
|
||||
this.conversation.map(k => k.id).map(subTreeDepthFor)
|
||||
this.conversation.map((k) => k.id).map(subTreeDepthFor)
|
||||
return Object.keys(depths).reduce((res, id) => {
|
||||
res[id] = depths[id] - 1 // exclude itself
|
||||
return res
|
||||
}, {})
|
||||
},
|
||||
depths () {
|
||||
depths() {
|
||||
return this.threadTree.reduce((a, k) => {
|
||||
a[k.id] = k.depth
|
||||
return a
|
||||
}, {})
|
||||
},
|
||||
topLevel () {
|
||||
const topLevel = this.conversation.reduce((tl, cur) =>
|
||||
tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation)
|
||||
topLevel() {
|
||||
const topLevel = this.conversation.reduce(
|
||||
(tl, cur) =>
|
||||
tl.filter(
|
||||
(k) =>
|
||||
this.getReplies(cur.id)
|
||||
.map((v) => v.id)
|
||||
.indexOf(k.id) === -1,
|
||||
),
|
||||
this.conversation,
|
||||
)
|
||||
return topLevel
|
||||
},
|
||||
otherTopLevelCount () {
|
||||
otherTopLevelCount() {
|
||||
return this.topLevel.length - 1
|
||||
},
|
||||
showingTopLevel () {
|
||||
showingTopLevel() {
|
||||
if (this.canDive && this.diveRoot) {
|
||||
return [this.statusMap[this.diveRoot]]
|
||||
}
|
||||
return this.topLevel
|
||||
},
|
||||
diveRoot () {
|
||||
diveRoot() {
|
||||
const statusId = this.inlineDivePosition || this.statusId
|
||||
const isTopLevel = !this.parentOf(statusId)
|
||||
return isTopLevel ? null : statusId
|
||||
},
|
||||
diveDepth () {
|
||||
diveDepth() {
|
||||
return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
|
||||
},
|
||||
diveMode () {
|
||||
diveMode() {
|
||||
return this.canDive && !!this.diveRoot
|
||||
},
|
||||
shouldShowAllConversationButton () {
|
||||
shouldShowAllConversationButton() {
|
||||
// The "show all conversation" button tells the user that there exist
|
||||
// other toplevel statuses, so do not show it if there is only a single root
|
||||
return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1
|
||||
return (
|
||||
this.isTreeView &&
|
||||
this.isExpanded &&
|
||||
this.diveMode &&
|
||||
this.topLevel.length > 1
|
||||
)
|
||||
},
|
||||
shouldShowAncestors () {
|
||||
return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length
|
||||
shouldShowAncestors() {
|
||||
return (
|
||||
this.isTreeView &&
|
||||
this.isExpanded &&
|
||||
this.ancestorsOf(this.diveRoot).length
|
||||
)
|
||||
},
|
||||
replies () {
|
||||
replies() {
|
||||
let i = 1
|
||||
|
||||
return reduce(this.conversation, (result, { id, in_reply_to_status_id: irid }) => {
|
||||
if (irid) {
|
||||
result[irid] = result[irid] || []
|
||||
result[irid].push({
|
||||
name: `#${i}`,
|
||||
id
|
||||
})
|
||||
}
|
||||
i++
|
||||
return result
|
||||
}, {})
|
||||
return reduce(
|
||||
this.conversation,
|
||||
(result, { id, in_reply_to_status_id: irid }) => {
|
||||
if (irid) {
|
||||
result[irid] = result[irid] || []
|
||||
result[irid].push({
|
||||
name: `#${i}`,
|
||||
id,
|
||||
})
|
||||
}
|
||||
i++
|
||||
return result
|
||||
},
|
||||
{},
|
||||
)
|
||||
},
|
||||
isExpanded () {
|
||||
isExpanded() {
|
||||
return !!(this.expanded || this.isPage)
|
||||
},
|
||||
hiddenStyle () {
|
||||
hiddenStyle() {
|
||||
const height = (this.status && this.status.virtualHeight) || '120px'
|
||||
return this.virtualHidden ? { height } : {}
|
||||
},
|
||||
threadDisplayStatus () {
|
||||
threadDisplayStatus() {
|
||||
return this.conversation.reduce((a, k) => {
|
||||
const id = k.id
|
||||
const depth = this.depths[id]
|
||||
|
|
@ -300,7 +349,7 @@ const conversation = {
|
|||
if (this.threadDisplayStatusObject[id]) {
|
||||
return this.threadDisplayStatusObject[id]
|
||||
}
|
||||
if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) {
|
||||
if (depth - this.diveDepth <= this.maxDepthToShowByDefault) {
|
||||
return 'showing'
|
||||
} else {
|
||||
return 'hidden'
|
||||
|
|
@ -311,7 +360,7 @@ const conversation = {
|
|||
return a
|
||||
}, {})
|
||||
},
|
||||
statusContentProperties () {
|
||||
statusContentProperties() {
|
||||
return this.conversation.reduce((a, k) => {
|
||||
const id = k.id
|
||||
const props = (() => {
|
||||
|
|
@ -320,13 +369,13 @@ const conversation = {
|
|||
expandingSubject: false,
|
||||
showingLongSubject: false,
|
||||
isReplying: false,
|
||||
mediaPlaying: []
|
||||
mediaPlaying: [],
|
||||
}
|
||||
|
||||
if (this.statusContentPropertiesObject[id]) {
|
||||
return {
|
||||
...def,
|
||||
...this.statusContentPropertiesObject[id]
|
||||
...this.statusContentPropertiesObject[id],
|
||||
}
|
||||
}
|
||||
return def
|
||||
|
|
@ -336,54 +385,59 @@ const conversation = {
|
|||
return a
|
||||
}, {})
|
||||
},
|
||||
canDive () {
|
||||
canDive() {
|
||||
return this.isTreeView && this.isExpanded
|
||||
},
|
||||
maybeHighlight () {
|
||||
maybeHighlight() {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
},
|
||||
...mapGetters(['mergedConfig']),
|
||||
...mapState({
|
||||
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus
|
||||
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
|
||||
}),
|
||||
...mapPiniaState(useInterfaceStore, {
|
||||
mobileLayout: store => store.layoutType === 'mobile'
|
||||
})
|
||||
mobileLayout: (store) => store.layoutType === 'mobile',
|
||||
}),
|
||||
},
|
||||
components: {
|
||||
Status,
|
||||
ThreadTree,
|
||||
QuickFilterSettings,
|
||||
QuickViewSettings
|
||||
QuickViewSettings,
|
||||
},
|
||||
watch: {
|
||||
statusId (newVal, oldVal) {
|
||||
statusId(newVal, oldVal) {
|
||||
const newConversationId = this.getConversationId(newVal)
|
||||
const oldConversationId = this.getConversationId(oldVal)
|
||||
if (newConversationId && oldConversationId && newConversationId === oldConversationId) {
|
||||
if (
|
||||
newConversationId &&
|
||||
oldConversationId &&
|
||||
newConversationId === oldConversationId
|
||||
) {
|
||||
this.setHighlight(this.originalStatusId)
|
||||
} else {
|
||||
this.fetchConversation()
|
||||
}
|
||||
},
|
||||
expanded (value) {
|
||||
expanded(value) {
|
||||
if (value) {
|
||||
this.fetchConversation()
|
||||
} else {
|
||||
this.resetDisplayState()
|
||||
}
|
||||
},
|
||||
virtualHidden () {
|
||||
this.$store.dispatch(
|
||||
'setVirtualHeight',
|
||||
{ statusId: this.statusId, height: `${this.$el.clientHeight}px` }
|
||||
)
|
||||
}
|
||||
virtualHidden() {
|
||||
this.$store.dispatch('setVirtualHeight', {
|
||||
statusId: this.statusId,
|
||||
height: `${this.$el.clientHeight}px`,
|
||||
})
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchConversation () {
|
||||
fetchConversation() {
|
||||
if (this.status) {
|
||||
this.$store.state.api.backendInteractor.fetchConversation({ id: this.statusId })
|
||||
this.$store.state.api.backendInteractor
|
||||
.fetchConversation({ id: this.statusId })
|
||||
.then(({ ancestors, descendants }) => {
|
||||
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
||||
this.$store.dispatch('addNewStatuses', { statuses: descendants })
|
||||
|
|
@ -391,7 +445,8 @@ const conversation = {
|
|||
})
|
||||
} else {
|
||||
this.loadStatusError = null
|
||||
this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId })
|
||||
this.$store.state.api.backendInteractor
|
||||
.fetchStatus({ id: this.statusId })
|
||||
.then((status) => {
|
||||
this.$store.dispatch('addNewStatuses', { statuses: [status] })
|
||||
this.fetchConversation()
|
||||
|
|
@ -401,16 +456,16 @@ const conversation = {
|
|||
})
|
||||
}
|
||||
},
|
||||
isFocused (id) {
|
||||
return (this.isExpanded) && id === this.highlight
|
||||
isFocused(id) {
|
||||
return this.isExpanded && id === this.highlight
|
||||
},
|
||||
getReplies (id) {
|
||||
getReplies(id) {
|
||||
return this.replies[id] || []
|
||||
},
|
||||
getHighlight () {
|
||||
getHighlight() {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
},
|
||||
setHighlight (id) {
|
||||
setHighlight(id) {
|
||||
if (!id) return
|
||||
this.highlight = id
|
||||
|
||||
|
|
@ -421,44 +476,54 @@ const conversation = {
|
|||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
||||
},
|
||||
toggleExpanded () {
|
||||
toggleExpanded() {
|
||||
this.expanded = !this.expanded
|
||||
},
|
||||
getConversationId (statusId) {
|
||||
getConversationId(statusId) {
|
||||
const status = this.$store.state.statuses.allStatusesObject[statusId]
|
||||
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
|
||||
return get(
|
||||
status,
|
||||
'retweeted_status.statusnet_conversation_id',
|
||||
get(status, 'statusnet_conversation_id'),
|
||||
)
|
||||
},
|
||||
setThreadDisplay (id, nextStatus) {
|
||||
setThreadDisplay(id, nextStatus) {
|
||||
this.threadDisplayStatusObject = {
|
||||
...this.threadDisplayStatusObject,
|
||||
[id]: nextStatus
|
||||
[id]: nextStatus,
|
||||
}
|
||||
},
|
||||
toggleThreadDisplay (id) {
|
||||
toggleThreadDisplay(id) {
|
||||
const curStatus = this.threadDisplayStatus[id]
|
||||
const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
|
||||
this.setThreadDisplay(id, nextStatus)
|
||||
},
|
||||
setThreadDisplayRecursively (id, nextStatus) {
|
||||
setThreadDisplayRecursively(id, nextStatus) {
|
||||
this.setThreadDisplay(id, nextStatus)
|
||||
this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus))
|
||||
this.getReplies(id)
|
||||
.map((k) => k.id)
|
||||
.map((id) => this.setThreadDisplayRecursively(id, nextStatus))
|
||||
},
|
||||
showThreadRecursively (id) {
|
||||
showThreadRecursively(id) {
|
||||
this.setThreadDisplayRecursively(id, 'showing')
|
||||
},
|
||||
setStatusContentProperty (id, name, value) {
|
||||
setStatusContentProperty(id, name, value) {
|
||||
this.statusContentPropertiesObject = {
|
||||
...this.statusContentPropertiesObject,
|
||||
[id]: {
|
||||
...this.statusContentPropertiesObject[id],
|
||||
[name]: value
|
||||
}
|
||||
[name]: value,
|
||||
},
|
||||
}
|
||||
},
|
||||
toggleStatusContentProperty (id, name) {
|
||||
this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
|
||||
toggleStatusContentProperty(id, name) {
|
||||
this.setStatusContentProperty(
|
||||
id,
|
||||
name,
|
||||
!this.statusContentProperties[id][name],
|
||||
)
|
||||
},
|
||||
leastVisibleAncestor (id) {
|
||||
leastVisibleAncestor(id) {
|
||||
let cur = id
|
||||
let parent = this.parentOf(cur)
|
||||
while (cur) {
|
||||
|
|
@ -472,18 +537,20 @@ const conversation = {
|
|||
// nothing found, fall back to toplevel
|
||||
return this.topLevel[0] ? this.topLevel[0].id : undefined
|
||||
},
|
||||
diveIntoStatus (id) {
|
||||
diveIntoStatus(id) {
|
||||
this.tryScrollTo(id)
|
||||
},
|
||||
diveToTopLevel () {
|
||||
this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id)
|
||||
diveToTopLevel() {
|
||||
this.tryScrollTo(
|
||||
this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id,
|
||||
)
|
||||
},
|
||||
// only used when we are not on a page
|
||||
undive () {
|
||||
undive() {
|
||||
this.inlineDivePosition = null
|
||||
this.setHighlight(this.statusId)
|
||||
},
|
||||
tryScrollTo (id) {
|
||||
tryScrollTo(id) {
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
|
|
@ -512,13 +579,13 @@ const conversation = {
|
|||
this.setHighlight(id)
|
||||
})
|
||||
},
|
||||
goToCurrent () {
|
||||
goToCurrent() {
|
||||
this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
|
||||
},
|
||||
statusById (id) {
|
||||
statusById(id) {
|
||||
return this.statusMap[id]
|
||||
},
|
||||
parentOf (id) {
|
||||
parentOf(id) {
|
||||
const status = this.statusById(id)
|
||||
if (!status) {
|
||||
return undefined
|
||||
|
|
@ -529,11 +596,11 @@ const conversation = {
|
|||
}
|
||||
return parentId
|
||||
},
|
||||
parentOrSelf (id) {
|
||||
parentOrSelf(id) {
|
||||
return this.parentOf(id) || id
|
||||
},
|
||||
// Ancestors of some status, from top to bottom
|
||||
ancestorsOf (id) {
|
||||
ancestorsOf(id) {
|
||||
const ancestors = []
|
||||
let cur = this.parentOf(id)
|
||||
while (cur) {
|
||||
|
|
@ -542,7 +609,7 @@ const conversation = {
|
|||
}
|
||||
return ancestors
|
||||
},
|
||||
topLevelAncestorOrSelfId (id) {
|
||||
topLevelAncestorOrSelfId(id) {
|
||||
let cur = id
|
||||
let parent = this.parentOf(id)
|
||||
while (parent) {
|
||||
|
|
@ -551,11 +618,11 @@ const conversation = {
|
|||
}
|
||||
return cur
|
||||
},
|
||||
resetDisplayState () {
|
||||
resetDisplayState() {
|
||||
this.undive()
|
||||
this.threadDisplayStatusObject = {}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default conversation
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
import SearchBar from 'components/search_bar/search_bar.vue'
|
||||
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faBell,
|
||||
faBullhorn,
|
||||
faCog,
|
||||
faComments,
|
||||
faHome,
|
||||
faInfoCircle,
|
||||
faSearch,
|
||||
faSignInAlt,
|
||||
faSignOutAlt,
|
||||
faHome,
|
||||
faComments,
|
||||
faBell,
|
||||
faUserPlus,
|
||||
faBullhorn,
|
||||
faSearch,
|
||||
faTachometerAlt,
|
||||
faCog,
|
||||
faInfoCircle
|
||||
faUserPlus,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
library.add(
|
||||
faSignInAlt,
|
||||
|
|
@ -27,91 +29,109 @@ library.add(
|
|||
faSearch,
|
||||
faTachometerAlt,
|
||||
faCog,
|
||||
faInfoCircle
|
||||
faInfoCircle,
|
||||
)
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SearchBar,
|
||||
ConfirmModal
|
||||
ConfirmModal,
|
||||
},
|
||||
data: () => ({
|
||||
searchBarHidden: true,
|
||||
supportsMask: window.CSS && window.CSS.supports && (
|
||||
window.CSS.supports('mask-size', 'contain') ||
|
||||
supportsMask:
|
||||
window.CSS &&
|
||||
window.CSS.supports &&
|
||||
(window.CSS.supports('mask-size', 'contain') ||
|
||||
window.CSS.supports('-webkit-mask-size', 'contain') ||
|
||||
window.CSS.supports('-moz-mask-size', 'contain') ||
|
||||
window.CSS.supports('-ms-mask-size', 'contain') ||
|
||||
window.CSS.supports('-o-mask-size', 'contain')
|
||||
),
|
||||
showingConfirmLogout: false
|
||||
window.CSS.supports('-o-mask-size', 'contain')),
|
||||
showingConfirmLogout: false,
|
||||
}),
|
||||
computed: {
|
||||
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
|
||||
logoStyle () {
|
||||
enableMask() {
|
||||
return this.supportsMask && this.$store.state.instance.logoMask
|
||||
},
|
||||
logoStyle() {
|
||||
return {
|
||||
visibility: this.enableMask ? 'hidden' : 'visible'
|
||||
visibility: this.enableMask ? 'hidden' : 'visible',
|
||||
}
|
||||
},
|
||||
logoMaskStyle () {
|
||||
logoMaskStyle() {
|
||||
return this.enableMask
|
||||
? {
|
||||
'mask-image': `url(${this.$store.state.instance.logo})`
|
||||
'mask-image': `url(${this.$store.state.instance.logo})`,
|
||||
}
|
||||
: {
|
||||
'background-color': this.enableMask ? '' : 'transparent'
|
||||
'background-color': this.enableMask ? '' : 'transparent',
|
||||
}
|
||||
},
|
||||
logoBgStyle () {
|
||||
return Object.assign({
|
||||
margin: `${this.$store.state.instance.logoMargin} 0`,
|
||||
opacity: this.searchBarHidden ? 1 : 0
|
||||
}, this.enableMask
|
||||
? {}
|
||||
: {
|
||||
'background-color': this.enableMask ? '' : 'transparent'
|
||||
})
|
||||
logoBgStyle() {
|
||||
return Object.assign(
|
||||
{
|
||||
margin: `${this.$store.state.instance.logoMargin} 0`,
|
||||
opacity: this.searchBarHidden ? 1 : 0,
|
||||
},
|
||||
this.enableMask
|
||||
? {}
|
||||
: {
|
||||
'background-color': this.enableMask ? '' : 'transparent',
|
||||
},
|
||||
)
|
||||
},
|
||||
logo () { return this.$store.state.instance.logo },
|
||||
sitename () { return this.$store.state.instance.name },
|
||||
hideSitename () { return this.$store.state.instance.hideSitename },
|
||||
logoLeft () { return this.$store.state.instance.logoLeft },
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
privateMode () { return this.$store.state.instance.private },
|
||||
shouldConfirmLogout () {
|
||||
logo() {
|
||||
return this.$store.state.instance.logo
|
||||
},
|
||||
sitename() {
|
||||
return this.$store.state.instance.name
|
||||
},
|
||||
hideSitename() {
|
||||
return this.$store.state.instance.hideSitename
|
||||
},
|
||||
logoLeft() {
|
||||
return this.$store.state.instance.logoLeft
|
||||
},
|
||||
currentUser() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
privateMode() {
|
||||
return this.$store.state.instance.private
|
||||
},
|
||||
shouldConfirmLogout() {
|
||||
return this.$store.getters.mergedConfig.modalOnLogout
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
scrollToTop () {
|
||||
scrollToTop() {
|
||||
window.scrollTo(0, 0)
|
||||
},
|
||||
showConfirmLogout () {
|
||||
showConfirmLogout() {
|
||||
this.showingConfirmLogout = true
|
||||
},
|
||||
hideConfirmLogout () {
|
||||
hideConfirmLogout() {
|
||||
this.showingConfirmLogout = false
|
||||
},
|
||||
logout () {
|
||||
logout() {
|
||||
if (!this.shouldConfirmLogout) {
|
||||
this.doLogout()
|
||||
} else {
|
||||
this.showConfirmLogout()
|
||||
}
|
||||
},
|
||||
doLogout () {
|
||||
doLogout() {
|
||||
this.$router.replace('/main/public')
|
||||
this.$store.dispatch('logout')
|
||||
this.hideConfirmLogout()
|
||||
},
|
||||
onSearchBarToggled (hidden) {
|
||||
onSearchBarToggled(hidden) {
|
||||
this.searchBarHidden = hidden
|
||||
},
|
||||
openSettingsModal () {
|
||||
openSettingsModal() {
|
||||
useInterfaceStore().openSettingsModal('user')
|
||||
},
|
||||
openAdminModal () {
|
||||
openAdminModal() {
|
||||
useInterfaceStore().openSettingsModal('admin')
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,18 +2,20 @@ const DialogModal = {
|
|||
props: {
|
||||
darkOverlay: {
|
||||
default: true,
|
||||
type: Boolean
|
||||
type: Boolean,
|
||||
},
|
||||
onCancel: {
|
||||
default: () => {},
|
||||
type: Function
|
||||
}
|
||||
default: () => {
|
||||
/* no-op */
|
||||
},
|
||||
type: Function,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
mobileCenter () {
|
||||
mobileCenter() {
|
||||
return this.$store.getters.mergedConfig.modalMobileCenter
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default DialogModal
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ import Timeline from '../timeline/timeline.vue'
|
|||
|
||||
const DMs = {
|
||||
computed: {
|
||||
timeline () {
|
||||
timeline() {
|
||||
return this.$store.state.statuses.timelines.dms
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Timeline
|
||||
}
|
||||
Timeline,
|
||||
},
|
||||
}
|
||||
|
||||
export default DMs
|
||||
|
|
|
|||
|
|
@ -3,24 +3,24 @@ import ProgressButton from '../progress_button/progress_button.vue'
|
|||
const DomainMuteCard = {
|
||||
props: ['domain'],
|
||||
components: {
|
||||
ProgressButton
|
||||
ProgressButton,
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
user() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
muted () {
|
||||
muted() {
|
||||
return this.user.domainMutes.includes(this.domain)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
unmuteDomain () {
|
||||
unmuteDomain() {
|
||||
return this.$store.dispatch('unmuteDomain', this.domain)
|
||||
},
|
||||
muteDomain () {
|
||||
muteDomain() {
|
||||
return this.$store.dispatch('muteDomain', this.domain)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default DomainMuteCard
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
|
||||
import EditStatusForm from 'src/components/edit_status_form/edit_status_form.vue'
|
||||
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
|
||||
import StatusContent from 'src/components/status_content/status_content.vue'
|
||||
import Gallery from 'src/components/gallery/gallery.vue'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faPollH
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
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'
|
||||
|
||||
library.add(
|
||||
faPollH
|
||||
)
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faPollH } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faPollH)
|
||||
|
||||
const Draft = {
|
||||
components: {
|
||||
|
|
@ -20,23 +17,23 @@ const Draft = {
|
|||
EditStatusForm,
|
||||
ConfirmModal,
|
||||
StatusContent,
|
||||
Gallery
|
||||
Gallery,
|
||||
},
|
||||
props: {
|
||||
draft: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
referenceDraft: cloneDeep(this.draft),
|
||||
editing: false,
|
||||
showingConfirmDialog: false
|
||||
showingConfirmDialog: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
relAttrs () {
|
||||
relAttrs() {
|
||||
if (this.draft.type === 'edit') {
|
||||
return { statusId: this.draft.refId }
|
||||
} else if (this.draft.type === 'reply') {
|
||||
|
|
@ -45,24 +42,24 @@ const Draft = {
|
|||
return {}
|
||||
}
|
||||
},
|
||||
safeToSave () {
|
||||
return this.draft.status ||
|
||||
this.draft.files?.length ||
|
||||
this.draft.hasPoll
|
||||
safeToSave() {
|
||||
return this.draft.status || this.draft.files?.length || this.draft.hasPoll
|
||||
},
|
||||
postStatusFormProps () {
|
||||
postStatusFormProps() {
|
||||
return {
|
||||
draftId: this.draft.id,
|
||||
...this.relAttrs
|
||||
...this.relAttrs,
|
||||
}
|
||||
},
|
||||
refStatus () {
|
||||
return this.draft.refId ? this.$store.state.statuses.allStatusesObject[this.draft.refId] : undefined
|
||||
refStatus() {
|
||||
return this.draft.refId
|
||||
? this.$store.state.statuses.allStatusesObject[this.draft.refId]
|
||||
: undefined
|
||||
},
|
||||
localCollapseSubjectDefault () {
|
||||
localCollapseSubjectDefault() {
|
||||
return this.$store.getters.mergedConfig.collapseMessageWithSubject
|
||||
},
|
||||
nsfwClickthrough () {
|
||||
nsfwClickthrough() {
|
||||
if (!this.draft.nsfw) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -70,35 +67,34 @@ const Draft = {
|
|||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
editing (newVal) {
|
||||
editing(newVal) {
|
||||
if (newVal) return
|
||||
if (this.safeToSave) {
|
||||
this.$store.dispatch('addOrSaveDraft', { draft: this.draft })
|
||||
} else {
|
||||
this.$store.dispatch('addOrSaveDraft', { draft: this.referenceDraft })
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleEditing () {
|
||||
toggleEditing() {
|
||||
this.editing = !this.editing
|
||||
},
|
||||
abandon () {
|
||||
abandon() {
|
||||
this.showingConfirmDialog = true
|
||||
},
|
||||
doAbandon () {
|
||||
this.$store.dispatch('abandonDraft', { id: this.draft.id })
|
||||
.then(() => {
|
||||
this.hideConfirmDialog()
|
||||
})
|
||||
doAbandon() {
|
||||
this.$store.dispatch('abandonDraft', { id: this.draft.id }).then(() => {
|
||||
this.hideConfirmDialog()
|
||||
})
|
||||
},
|
||||
hideConfirmDialog () {
|
||||
hideConfirmDialog() {
|
||||
this.showingConfirmDialog = false
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default Draft
|
||||
|
|
|
|||
|
|
@ -1,32 +1,29 @@
|
|||
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue'
|
||||
|
||||
const DraftCloser = {
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
showing: false
|
||||
showing: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
DialogModal
|
||||
DialogModal,
|
||||
},
|
||||
emits: [
|
||||
'save',
|
||||
'discard'
|
||||
],
|
||||
emits: ['save', 'discard'],
|
||||
computed: {
|
||||
action () {
|
||||
action() {
|
||||
if (this.$store.getters.mergedConfig.autoSaveDraft) {
|
||||
return 'save'
|
||||
} else {
|
||||
return this.$store.getters.mergedConfig.unsavedPostAction
|
||||
}
|
||||
},
|
||||
shouldConfirm () {
|
||||
shouldConfirm() {
|
||||
return this.action === 'confirm'
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
requestClose () {
|
||||
requestClose() {
|
||||
if (this.shouldConfirm) {
|
||||
this.showing = true
|
||||
} else if (this.action === 'save') {
|
||||
|
|
@ -35,18 +32,18 @@ const DraftCloser = {
|
|||
this.discard()
|
||||
}
|
||||
},
|
||||
save () {
|
||||
save() {
|
||||
this.$emit('save')
|
||||
this.showing = false
|
||||
},
|
||||
discard () {
|
||||
discard() {
|
||||
this.$emit('discard')
|
||||
this.showing = false
|
||||
},
|
||||
cancel () {
|
||||
cancel() {
|
||||
this.showing = false
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default DraftCloser
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import List from 'src/components/list/list.vue'
|
|||
const Drafts = {
|
||||
components: {
|
||||
Draft,
|
||||
List
|
||||
List,
|
||||
},
|
||||
computed: {
|
||||
drafts () {
|
||||
drafts() {
|
||||
return this.$store.getters.draftsArray
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default Drafts
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import PostStatusForm from '../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: {
|
||||
PostStatusForm
|
||||
PostStatusForm,
|
||||
},
|
||||
props: {
|
||||
params: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
requestClose () {
|
||||
requestClose() {
|
||||
this.$refs.postStatusForm.requestClose()
|
||||
},
|
||||
doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
|
||||
doEditStatus({ status, spoilerText, sensitive, media, contentType, poll }) {
|
||||
const params = {
|
||||
store: this.$store,
|
||||
statusId: this.params.statusId,
|
||||
|
|
@ -24,21 +24,22 @@ const EditStatusForm = {
|
|||
sensitive,
|
||||
poll,
|
||||
media,
|
||||
contentType
|
||||
contentType,
|
||||
}
|
||||
|
||||
return statusPosterService.editStatus(params)
|
||||
return statusPosterService
|
||||
.editStatus(params)
|
||||
.then((data) => {
|
||||
return data
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Error editing status', err)
|
||||
return {
|
||||
error: err.message
|
||||
error: err.message,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default EditStatusForm
|
||||
|
|
|
|||
|
|
@ -1,34 +1,35 @@
|
|||
import get from 'lodash/get'
|
||||
|
||||
import { useEditStatusStore } from 'src/stores/editStatus'
|
||||
import EditStatusForm from '../edit_status_form/edit_status_form.vue'
|
||||
import Modal from '../modal/modal.vue'
|
||||
import get from 'lodash/get'
|
||||
import { useEditStatusStore } from 'src/stores/editStatus'
|
||||
|
||||
const EditStatusModal = {
|
||||
components: {
|
||||
EditStatusForm,
|
||||
Modal
|
||||
Modal,
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
resettingForm: false
|
||||
resettingForm: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isLoggedIn () {
|
||||
isLoggedIn() {
|
||||
return !!this.$store.state.users.currentUser
|
||||
},
|
||||
modalActivated () {
|
||||
modalActivated() {
|
||||
return useEditStatusStore().modalActivated
|
||||
},
|
||||
isFormVisible () {
|
||||
isFormVisible() {
|
||||
return this.isLoggedIn && !this.resettingForm && this.modalActivated
|
||||
},
|
||||
params () {
|
||||
params() {
|
||||
return useEditStatusStore().params || {}
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
params (newVal, oldVal) {
|
||||
params(newVal, oldVal) {
|
||||
if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
|
||||
this.resettingForm = true
|
||||
this.$nextTick(() => {
|
||||
|
|
@ -36,20 +37,22 @@ const EditStatusModal = {
|
|||
})
|
||||
}
|
||||
},
|
||||
isFormVisible (val) {
|
||||
isFormVisible(val) {
|
||||
if (val) {
|
||||
this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
|
||||
this.$nextTick(
|
||||
() => this.$el && this.$el.querySelector('textarea').focus(),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeModal () {
|
||||
closeModal() {
|
||||
this.$refs.editStatusForm.requestClose()
|
||||
},
|
||||
doCloseModal () {
|
||||
doCloseModal() {
|
||||
useEditStatusStore().closeEditStatusModal()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default EditStatusModal
|
||||
|
|
|
|||
|
|
@ -1,20 +1,18 @@
|
|||
import Completion from '../../services/completion/completion.js'
|
||||
import genRandomSeed from '../../services/random_seed/random_seed.service.js'
|
||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||
import { take } from 'lodash'
|
||||
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import ScreenReaderNotice from 'src/components/screen_reader_notice/screen_reader_notice.vue'
|
||||
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||
import { take } from 'lodash'
|
||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||
import { ensureFinalFallback } from '../../i18n/languages.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faSmileBeam
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
import Completion from '../../services/completion/completion.js'
|
||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||
import genRandomSeed from '../../services/random_seed/random_seed.service.js'
|
||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||
|
||||
library.add(
|
||||
faSmileBeam
|
||||
)
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(faSmileBeam)
|
||||
|
||||
/**
|
||||
* EmojiInput - augmented inputs for emoji and autocomplete support in inputs
|
||||
|
|
@ -60,14 +58,14 @@ const EmojiInput = {
|
|||
* For commonly used suggestors (emoji, users, both) use suggestor.js
|
||||
*/
|
||||
required: true,
|
||||
type: Function
|
||||
type: Function,
|
||||
},
|
||||
modelValue: {
|
||||
/**
|
||||
* Used for v-model
|
||||
*/
|
||||
required: true,
|
||||
type: String
|
||||
type: String,
|
||||
},
|
||||
enableEmojiPicker: {
|
||||
/**
|
||||
|
|
@ -75,7 +73,7 @@ const EmojiInput = {
|
|||
*/
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
hideEmojiButton: {
|
||||
/**
|
||||
|
|
@ -84,7 +82,7 @@ const EmojiInput = {
|
|||
*/
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
enableStickerPicker: {
|
||||
/**
|
||||
|
|
@ -92,7 +90,7 @@ const EmojiInput = {
|
|||
*/
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
placement: {
|
||||
/**
|
||||
|
|
@ -101,15 +99,15 @@ const EmojiInput = {
|
|||
*/
|
||||
required: false,
|
||||
type: String, // 'auto', 'top', 'bottom'
|
||||
default: 'auto'
|
||||
default: 'auto',
|
||||
},
|
||||
newlineOnCtrlEnter: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
randomSeed: genRandomSeed(),
|
||||
input: undefined,
|
||||
|
|
@ -122,58 +120,65 @@ const EmojiInput = {
|
|||
disableClickOutside: false,
|
||||
suggestions: [],
|
||||
overlayStyle: {},
|
||||
pickerShown: false
|
||||
pickerShown: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Popover,
|
||||
EmojiPicker,
|
||||
UnicodeDomainIndicator,
|
||||
ScreenReaderNotice
|
||||
ScreenReaderNotice,
|
||||
},
|
||||
computed: {
|
||||
padEmoji () {
|
||||
padEmoji() {
|
||||
return this.$store.getters.mergedConfig.padEmoji
|
||||
},
|
||||
defaultCandidateIndex () {
|
||||
defaultCandidateIndex() {
|
||||
return this.$store.getters.mergedConfig.autocompleteSelect ? 0 : -1
|
||||
},
|
||||
preText () {
|
||||
preText() {
|
||||
return this.modelValue.slice(0, this.caret)
|
||||
},
|
||||
postText () {
|
||||
postText() {
|
||||
return this.modelValue.slice(this.caret)
|
||||
},
|
||||
showSuggestions () {
|
||||
return this.focused &&
|
||||
showSuggestions() {
|
||||
return (
|
||||
this.focused &&
|
||||
this.suggestions &&
|
||||
this.suggestions.length > 0 &&
|
||||
!this.pickerShown &&
|
||||
!this.temporarilyHideSuggestions
|
||||
)
|
||||
},
|
||||
textAtCaret () {
|
||||
textAtCaret() {
|
||||
return this.wordAtCaret?.word
|
||||
},
|
||||
wordAtCaret () {
|
||||
wordAtCaret() {
|
||||
if (this.modelValue && this.caret) {
|
||||
const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
|
||||
const word =
|
||||
Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
|
||||
return word
|
||||
}
|
||||
},
|
||||
languages () {
|
||||
return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
|
||||
languages() {
|
||||
return ensureFinalFallback(
|
||||
this.$store.getters.mergedConfig.interfaceLanguage,
|
||||
)
|
||||
},
|
||||
maybeLocalizedEmojiNamesAndKeywords () {
|
||||
return emoji => {
|
||||
maybeLocalizedEmojiNamesAndKeywords() {
|
||||
return (emoji) => {
|
||||
const names = [emoji.displayText]
|
||||
const keywords = []
|
||||
|
||||
if (emoji.displayTextI18n) {
|
||||
names.push(this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args))
|
||||
names.push(
|
||||
this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args),
|
||||
)
|
||||
}
|
||||
|
||||
if (emoji.annotations) {
|
||||
this.languages.forEach(lang => {
|
||||
this.languages.forEach((lang) => {
|
||||
names.push(emoji.annotations[lang]?.name)
|
||||
|
||||
keywords.push(...(emoji.annotations[lang]?.keywords || []))
|
||||
|
|
@ -181,13 +186,13 @@ const EmojiInput = {
|
|||
}
|
||||
|
||||
return {
|
||||
names: names.filter(k => k),
|
||||
keywords: keywords.filter(k => k)
|
||||
names: names.filter((k) => k),
|
||||
keywords: keywords.filter((k) => k),
|
||||
}
|
||||
}
|
||||
},
|
||||
maybeLocalizedEmojiName () {
|
||||
return emoji => {
|
||||
maybeLocalizedEmojiName() {
|
||||
return (emoji) => {
|
||||
if (!emoji.annotations) {
|
||||
return emoji.displayText
|
||||
}
|
||||
|
|
@ -205,16 +210,18 @@ const EmojiInput = {
|
|||
return emoji.displayText
|
||||
}
|
||||
},
|
||||
suggestionListId () {
|
||||
suggestionListId() {
|
||||
return `suggestions-${this.randomSeed}`
|
||||
},
|
||||
suggestionItemId () {
|
||||
suggestionItemId() {
|
||||
return (index) => `suggestion-item-${index}-${this.randomSeed}`
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
const { root, hiddenOverlayCaret, suggestorPopover } = this.$refs
|
||||
const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea')
|
||||
const input =
|
||||
root.querySelector('.emoji-input > input') ||
|
||||
root.querySelector('.emoji-input > textarea')
|
||||
if (!input) return
|
||||
this.input = input
|
||||
this.caretEl = hiddenOverlayCaret
|
||||
|
|
@ -243,7 +250,7 @@ const EmojiInput = {
|
|||
input.addEventListener('input', this.onInput)
|
||||
input.addEventListener('scroll', this.onInputScroll)
|
||||
},
|
||||
unmounted () {
|
||||
unmounted() {
|
||||
const { input } = this
|
||||
if (input) {
|
||||
input.removeEventListener('blur', this.onBlur)
|
||||
|
|
@ -273,36 +280,40 @@ const EmojiInput = {
|
|||
this.suggestions = []
|
||||
return
|
||||
}
|
||||
const matchedSuggestions = await this.suggest(newWord, this.maybeLocalizedEmojiNamesAndKeywords)
|
||||
const matchedSuggestions = await this.suggest(
|
||||
newWord,
|
||||
this.maybeLocalizedEmojiNamesAndKeywords,
|
||||
)
|
||||
// Async: cancel if textAtCaret has changed during wait
|
||||
if (this.textAtCaret !== newWord || matchedSuggestions.length <= 0) {
|
||||
this.suggestions = []
|
||||
return
|
||||
}
|
||||
this.suggestions = take(matchedSuggestions, 5)
|
||||
.map(({ imageUrl, ...rest }) => ({
|
||||
this.suggestions = take(matchedSuggestions, 5).map(
|
||||
({ imageUrl, ...rest }) => ({
|
||||
...rest,
|
||||
img: imageUrl || ''
|
||||
}))
|
||||
img: imageUrl || '',
|
||||
}),
|
||||
)
|
||||
this.highlighted = this.defaultCandidateIndex
|
||||
this.$refs.screenReaderNotice.announce(
|
||||
this.$t(
|
||||
'tool_tip.autocomplete_available',
|
||||
{ number: this.suggestions.length },
|
||||
this.suggestions.length
|
||||
)
|
||||
this.suggestions.length,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onInputScroll (e) {
|
||||
onInputScroll(e) {
|
||||
this.$refs.hiddenOverlay.scrollTo({
|
||||
top: this.input.scrollTop,
|
||||
left: this.input.scrollLeft
|
||||
left: this.input.scrollLeft,
|
||||
})
|
||||
this.setCaret(e)
|
||||
},
|
||||
triggerShowPicker () {
|
||||
triggerShowPicker() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.picker.showPicker()
|
||||
this.scrollIntoView()
|
||||
|
|
@ -315,7 +326,7 @@ const EmojiInput = {
|
|||
this.disableClickOutside = false
|
||||
}, 0)
|
||||
},
|
||||
togglePicker () {
|
||||
togglePicker() {
|
||||
this.input.focus()
|
||||
if (!this.pickerShown) {
|
||||
this.scrollIntoView()
|
||||
|
|
@ -325,12 +336,16 @@ const EmojiInput = {
|
|||
this.$refs.picker.hidePicker()
|
||||
}
|
||||
},
|
||||
replace (replacement) {
|
||||
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement)
|
||||
replace(replacement) {
|
||||
const newValue = Completion.replaceWord(
|
||||
this.modelValue,
|
||||
this.wordAtCaret,
|
||||
replacement,
|
||||
)
|
||||
this.$emit('update:modelValue', newValue)
|
||||
this.caret = 0
|
||||
},
|
||||
insert ({ insertion, keepOpen, surroundingSpace = true }) {
|
||||
insert({ insertion, keepOpen, surroundingSpace = true }) {
|
||||
const before = this.modelValue.substring(0, this.caret) || ''
|
||||
const after = this.modelValue.substring(this.caret) || ''
|
||||
|
||||
|
|
@ -349,18 +364,24 @@ const EmojiInput = {
|
|||
* them, masto seem to be rendering :emoji::emoji: correctly now so why not
|
||||
*/
|
||||
const isSpaceRegex = /\s/
|
||||
const spaceBefore = (surroundingSpace && !isSpaceRegex.exec(before.slice(-1)) && before.length && this.padEmoji > 0) ? ' ' : ''
|
||||
const spaceAfter = (surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji) ? ' ' : ''
|
||||
const spaceBefore =
|
||||
surroundingSpace &&
|
||||
!isSpaceRegex.exec(before.slice(-1)) &&
|
||||
before.length &&
|
||||
this.padEmoji > 0
|
||||
? ' '
|
||||
: ''
|
||||
const spaceAfter =
|
||||
surroundingSpace && !isSpaceRegex.exec(after[0]) && this.padEmoji
|
||||
? ' '
|
||||
: ''
|
||||
|
||||
const newValue = [
|
||||
before,
|
||||
spaceBefore,
|
||||
insertion,
|
||||
spaceAfter,
|
||||
after
|
||||
].join('')
|
||||
const newValue = [before, spaceBefore, insertion, spaceAfter, after].join(
|
||||
'',
|
||||
)
|
||||
this.$emit('update:modelValue', newValue)
|
||||
const position = this.caret + (insertion + spaceAfter + spaceBefore).length
|
||||
const position =
|
||||
this.caret + (insertion + spaceAfter + spaceBefore).length
|
||||
if (!keepOpen) {
|
||||
this.input.focus()
|
||||
}
|
||||
|
|
@ -372,13 +393,20 @@ const EmojiInput = {
|
|||
this.caret = position
|
||||
})
|
||||
},
|
||||
replaceText (e, suggestion) {
|
||||
replaceText(e, suggestion) {
|
||||
const len = this.suggestions.length || 0
|
||||
if (this.textAtCaret.length === 1) { return }
|
||||
if (this.textAtCaret.length === 1) {
|
||||
return
|
||||
}
|
||||
if (len > 0 || suggestion) {
|
||||
const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
|
||||
const chosenSuggestion =
|
||||
suggestion || this.suggestions[this.highlighted]
|
||||
const replacement = chosenSuggestion.replacement
|
||||
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement)
|
||||
const newValue = Completion.replaceWord(
|
||||
this.modelValue,
|
||||
this.wordAtCaret,
|
||||
replacement,
|
||||
)
|
||||
this.$emit('update:modelValue', newValue)
|
||||
this.highlighted = 0
|
||||
const position = this.wordAtCaret.start + replacement.length
|
||||
|
|
@ -393,7 +421,7 @@ const EmojiInput = {
|
|||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
cycleBackward (e) {
|
||||
cycleBackward(e) {
|
||||
const len = this.suggestions.length || 0
|
||||
|
||||
this.highlighted -= 1
|
||||
|
|
@ -406,7 +434,7 @@ const EmojiInput = {
|
|||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
cycleForward (e) {
|
||||
cycleForward(e) {
|
||||
const len = this.suggestions.length || 0
|
||||
|
||||
this.highlighted += 1
|
||||
|
|
@ -418,26 +446,28 @@ const EmojiInput = {
|
|||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
scrollIntoView () {
|
||||
scrollIntoView() {
|
||||
const rootRef = this.$refs.picker.$el
|
||||
/* Scroller is either `window` (replies in TL), sidebar (main post form,
|
||||
* replies in notifs) or mobile post form. Note that getting and setting
|
||||
* scroll is different for `Window` and `Element`s
|
||||
*/
|
||||
const scrollerRef = this.$el.closest('.sidebar-scroller') ||
|
||||
this.$el.closest('.post-form-modal-view') ||
|
||||
window
|
||||
const currentScroll = scrollerRef === window
|
||||
? scrollerRef.scrollY
|
||||
: scrollerRef.scrollTop
|
||||
const scrollerHeight = scrollerRef === window
|
||||
? scrollerRef.innerHeight
|
||||
: scrollerRef.offsetHeight
|
||||
const scrollerRef =
|
||||
this.$el.closest('.sidebar-scroller') ||
|
||||
this.$el.closest('.post-form-modal-view') ||
|
||||
window
|
||||
const currentScroll =
|
||||
scrollerRef === window ? scrollerRef.scrollY : scrollerRef.scrollTop
|
||||
const scrollerHeight =
|
||||
scrollerRef === window
|
||||
? scrollerRef.innerHeight
|
||||
: scrollerRef.offsetHeight
|
||||
|
||||
const scrollerBottomBorder = currentScroll + scrollerHeight
|
||||
// We check where the bottom border of root element is, this uses findOffset
|
||||
// to find offset relative to scrollable container (scroller)
|
||||
const rootBottomBorder = rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
|
||||
const rootBottomBorder =
|
||||
rootRef.offsetHeight + findOffset(rootRef, scrollerRef).top
|
||||
|
||||
const bottomDelta = Math.max(0, rootBottomBorder - scrollerBottomBorder)
|
||||
// could also check top delta but there's no case for it
|
||||
|
|
@ -459,13 +489,13 @@ const EmojiInput = {
|
|||
}
|
||||
})
|
||||
},
|
||||
onPickerShown () {
|
||||
onPickerShown() {
|
||||
this.pickerShown = true
|
||||
},
|
||||
onPickerClosed () {
|
||||
onPickerClosed() {
|
||||
this.pickerShown = false
|
||||
},
|
||||
onBlur (e) {
|
||||
onBlur(e) {
|
||||
// Clicking on any suggestion removes focus from autocomplete,
|
||||
// preventing click handler ever executing.
|
||||
this.blurTimeout = setTimeout(() => {
|
||||
|
|
@ -473,10 +503,10 @@ const EmojiInput = {
|
|||
this.setCaret(e)
|
||||
}, 200)
|
||||
},
|
||||
onClick (e, suggestion) {
|
||||
onClick(e, suggestion) {
|
||||
this.replaceText(e, suggestion)
|
||||
},
|
||||
onFocus (e) {
|
||||
onFocus(e) {
|
||||
if (this.blurTimeout) {
|
||||
clearTimeout(this.blurTimeout)
|
||||
this.blurTimeout = null
|
||||
|
|
@ -486,7 +516,7 @@ const EmojiInput = {
|
|||
this.setCaret(e)
|
||||
this.temporarilyHideSuggestions = false
|
||||
},
|
||||
onKeyUp (e) {
|
||||
onKeyUp(e) {
|
||||
const { key } = e
|
||||
this.setCaret(e)
|
||||
|
||||
|
|
@ -498,10 +528,10 @@ const EmojiInput = {
|
|||
this.temporarilyHideSuggestions = false
|
||||
}
|
||||
},
|
||||
onPaste (e) {
|
||||
onPaste(e) {
|
||||
this.setCaret(e)
|
||||
},
|
||||
onKeyDown (e) {
|
||||
onKeyDown(e) {
|
||||
const { ctrlKey, shiftKey, key } = e
|
||||
if (this.newlineOnCtrlEnter && ctrlKey && key === 'Enter') {
|
||||
this.insert({ insertion: '\n', surroundingSpace: false })
|
||||
|
|
@ -545,30 +575,30 @@ const EmojiInput = {
|
|||
}
|
||||
}
|
||||
},
|
||||
onInput (e) {
|
||||
onInput(e) {
|
||||
this.setCaret(e)
|
||||
this.$emit('update:modelValue', e.target.value)
|
||||
},
|
||||
onStickerUploaded (e) {
|
||||
onStickerUploaded(e) {
|
||||
this.$emit('sticker-uploaded', e)
|
||||
},
|
||||
onStickerUploadFailed (e) {
|
||||
onStickerUploadFailed(e) {
|
||||
this.$emit('sticker-upload-Failed', e)
|
||||
},
|
||||
setCaret ({ target: { selectionStart } }) {
|
||||
setCaret({ target: { selectionStart } }) {
|
||||
this.caret = selectionStart
|
||||
this.$nextTick(() => {
|
||||
this.$refs.suggestorPopover.updateStyles()
|
||||
})
|
||||
},
|
||||
autoCompleteItemLabel (suggestion) {
|
||||
autoCompleteItemLabel(suggestion) {
|
||||
if (suggestion.user) {
|
||||
return suggestion.displayText + ' ' + suggestion.detailText
|
||||
} else {
|
||||
return this.maybeLocalizedEmojiName(suggestion)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default EmojiInput
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
* doesn't support user linking you can just provide only emoji.
|
||||
*/
|
||||
|
||||
export default data => {
|
||||
export default (data) => {
|
||||
const emojiCurry = suggestEmoji(data.emoji)
|
||||
const usersCurry = data.store && suggestUsers(data.store)
|
||||
return (input, nameKeywordLocalizer) => {
|
||||
|
|
@ -25,22 +25,35 @@ export default data => {
|
|||
}
|
||||
}
|
||||
|
||||
export const suggestEmoji = emojis => (input, nameKeywordLocalizer) => {
|
||||
export const suggestEmoji = (emojis) => (input, nameKeywordLocalizer) => {
|
||||
const noPrefix = input.toLowerCase().substr(1)
|
||||
return emojis
|
||||
.map(emoji => ({ ...emoji, ...nameKeywordLocalizer(emoji) }))
|
||||
.filter((emoji) => (emoji.names.concat(emoji.keywords)).filter(kw => kw.toLowerCase().match(noPrefix)).length)
|
||||
.map(k => {
|
||||
.map((emoji) => ({ ...emoji, ...nameKeywordLocalizer(emoji) }))
|
||||
.filter(
|
||||
(emoji) =>
|
||||
emoji.names
|
||||
.concat(emoji.keywords)
|
||||
.filter((kw) => kw.toLowerCase().match(noPrefix)).length,
|
||||
)
|
||||
.map((k) => {
|
||||
let score = 0
|
||||
|
||||
// An exact match always wins
|
||||
score += Math.max(...k.names.map(name => name.toLowerCase() === noPrefix ? 200 : 0), 0)
|
||||
score += Math.max(
|
||||
...k.names.map((name) => (name.toLowerCase() === noPrefix ? 200 : 0)),
|
||||
0,
|
||||
)
|
||||
|
||||
// Prioritize custom emoji a lot
|
||||
score += k.imageUrl ? 100 : 0
|
||||
|
||||
// Prioritize prefix matches somewhat
|
||||
score += Math.max(...k.names.map(kw => kw.toLowerCase().startsWith(noPrefix) ? 10 : 0), 0)
|
||||
score += Math.max(
|
||||
...k.names.map((kw) =>
|
||||
kw.toLowerCase().startsWith(noPrefix) ? 10 : 0,
|
||||
),
|
||||
0,
|
||||
)
|
||||
|
||||
// Sort by length
|
||||
score -= k.displayText.length
|
||||
|
|
@ -78,7 +91,7 @@ export const suggestUsers = ({ dispatch, state }) => {
|
|||
})
|
||||
}
|
||||
|
||||
return async input => {
|
||||
return async (input) => {
|
||||
const noPrefix = input.toLowerCase().substr(1)
|
||||
if (previousQuery === noPrefix) return suggestions
|
||||
|
||||
|
|
@ -92,37 +105,42 @@ export const suggestUsers = ({ dispatch, state }) => {
|
|||
await debounceUserSearch(noPrefix)
|
||||
}
|
||||
|
||||
const newSuggestions = state.users.users.filter(
|
||||
user =>
|
||||
user.screen_name && user.name && (
|
||||
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
||||
user.name.toLowerCase().startsWith(noPrefix))
|
||||
).slice(0, 20).sort((a, b) => {
|
||||
let aScore = 0
|
||||
let bScore = 0
|
||||
const newSuggestions = state.users.users
|
||||
.filter(
|
||||
(user) =>
|
||||
user.screen_name &&
|
||||
user.name &&
|
||||
(user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
||||
user.name.toLowerCase().startsWith(noPrefix)),
|
||||
)
|
||||
.slice(0, 20)
|
||||
.sort((a, b) => {
|
||||
let aScore = 0
|
||||
let bScore = 0
|
||||
|
||||
// Matches on screen name (i.e. user@instance) makes a priority
|
||||
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||
// Matches on screen name (i.e. user@instance) makes a priority
|
||||
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||
|
||||
// Matches on name takes second priority
|
||||
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||
// Matches on name takes second priority
|
||||
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||
|
||||
const diff = (bScore - aScore) * 10
|
||||
const diff = (bScore - aScore) * 10
|
||||
|
||||
// Then sort alphabetically
|
||||
const nameAlphabetically = a.name > b.name ? 1 : -1
|
||||
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
|
||||
// Then sort alphabetically
|
||||
const nameAlphabetically = a.name > b.name ? 1 : -1
|
||||
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
|
||||
|
||||
return diff + nameAlphabetically + screenNameAlphabetically
|
||||
}).map((user) => ({
|
||||
user,
|
||||
displayText: user.screen_name_ui,
|
||||
detailText: user.name,
|
||||
imageUrl: user.profile_image_url_original,
|
||||
replacement: '@' + user.screen_name + ' '
|
||||
}))
|
||||
return diff + nameAlphabetically + screenNameAlphabetically
|
||||
})
|
||||
.map((user) => ({
|
||||
user,
|
||||
displayText: user.screen_name_ui,
|
||||
detailText: user.name,
|
||||
imageUrl: user.profile_image_url_original,
|
||||
replacement: '@' + user.screen_name + ' ',
|
||||
}))
|
||||
|
||||
suggestions = newSuggestions || []
|
||||
return suggestions
|
||||
|
|
|
|||
|
|
@ -1,24 +1,26 @@
|
|||
import { chunk, debounce, trim } from 'lodash'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
import { ensureFinalFallback } from '../../i18n/languages.js'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faBoxOpen,
|
||||
faStickyNote,
|
||||
faSmileBeam,
|
||||
faSmile,
|
||||
faUser,
|
||||
faPaw,
|
||||
faIceCream,
|
||||
faBus,
|
||||
faBasketballBall,
|
||||
faLightbulb,
|
||||
faBoxOpen,
|
||||
faBus,
|
||||
faCode,
|
||||
faFlag
|
||||
faFlag,
|
||||
faIceCream,
|
||||
faLightbulb,
|
||||
faPaw,
|
||||
faSmile,
|
||||
faSmileBeam,
|
||||
faStickyNote,
|
||||
faUser,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { debounce, trim, chunk } from 'lodash'
|
||||
|
||||
library.add(
|
||||
faBoxOpen,
|
||||
|
|
@ -32,7 +34,7 @@ library.add(
|
|||
faBasketballBall,
|
||||
faLightbulb,
|
||||
faCode,
|
||||
faFlag
|
||||
faFlag,
|
||||
)
|
||||
|
||||
const UNICODE_EMOJI_GROUP_ICON = {
|
||||
|
|
@ -44,16 +46,16 @@ const UNICODE_EMOJI_GROUP_ICON = {
|
|||
activities: 'basketball-ball',
|
||||
objects: 'lightbulb',
|
||||
symbols: 'code',
|
||||
flags: 'flag'
|
||||
flags: 'flag',
|
||||
}
|
||||
|
||||
const maybeLocalizedKeywords = (emoji, languages, nameLocalizer) => {
|
||||
const res = [emoji.displayText, nameLocalizer(emoji)]
|
||||
if (emoji.annotations) {
|
||||
languages.forEach(lang => {
|
||||
languages.forEach((lang) => {
|
||||
const keywords = emoji.annotations[lang]?.keywords || []
|
||||
const name = emoji.annotations[lang]?.name
|
||||
res.push(...(keywords.concat([name]).filter(k => k)))
|
||||
res.push(...keywords.concat([name]).filter((k) => k))
|
||||
})
|
||||
}
|
||||
return res
|
||||
|
|
@ -66,8 +68,8 @@ const filterByKeyword = (list, keyword = '', languages, nameLocalizer) => {
|
|||
const orderedEmojiList = []
|
||||
for (const emoji of list) {
|
||||
const indices = maybeLocalizedKeywords(emoji, languages, nameLocalizer)
|
||||
.map(k => k.toLowerCase().indexOf(keywordLowercase))
|
||||
.filter(k => k > -1)
|
||||
.map((k) => k.toLowerCase().indexOf(keywordLowercase))
|
||||
.filter((k) => k > -1)
|
||||
|
||||
const indexOfKeyword = indices.length ? Math.min(...indices) : -1
|
||||
|
||||
|
|
@ -84,11 +86,13 @@ const filterByKeyword = (list, keyword = '', languages, nameLocalizer) => {
|
|||
const getOffset = (elem) => {
|
||||
const style = elem.style.transform
|
||||
const res = /translateY\((\d+)px\)/.exec(style)
|
||||
if (!res) { return 0 }
|
||||
if (!res) {
|
||||
return 0
|
||||
}
|
||||
return res[1]
|
||||
}
|
||||
|
||||
const toHeaderId = id => {
|
||||
const toHeaderId = (id) => {
|
||||
return id.replace(/^row-\d+-/, '')
|
||||
}
|
||||
|
||||
|
|
@ -97,20 +101,20 @@ const EmojiPicker = {
|
|||
enableStickerPicker: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: true
|
||||
default: true,
|
||||
},
|
||||
hideCustomEmoji: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
inject: {
|
||||
popoversZLayer: {
|
||||
default: ''
|
||||
}
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
keyword: '',
|
||||
activeGroup: 'custom',
|
||||
|
|
@ -125,20 +129,22 @@ const EmojiPicker = {
|
|||
emojiRefs: {},
|
||||
filteredEmojiGroups: [],
|
||||
emojiSize: 0,
|
||||
width: 0
|
||||
width: 0,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
||||
StickerPicker: defineAsyncComponent(
|
||||
() => import('../sticker_picker/sticker_picker.vue'),
|
||||
),
|
||||
Checkbox,
|
||||
StillImage,
|
||||
Popover
|
||||
Popover,
|
||||
},
|
||||
methods: {
|
||||
groupScroll (e) {
|
||||
groupScroll(e) {
|
||||
e.currentTarget.scrollLeft += e.deltaY + e.deltaX
|
||||
},
|
||||
updateEmojiSize () {
|
||||
updateEmojiSize() {
|
||||
const css = window.getComputedStyle(this.$refs.popover.$el)
|
||||
const fontSize = css.getPropertyValue('font-size') || '1rem'
|
||||
const emojiSize = css.getPropertyValue('--emojiSize') || '2.2rem'
|
||||
|
|
@ -163,56 +169,68 @@ const EmojiPicker = {
|
|||
emojiSizeReal = emojiSizeValue
|
||||
}
|
||||
|
||||
const fullEmojiSize = emojiSizeReal + (2 * 0.2 * fontSizeMultiplier * 14)
|
||||
const fullEmojiSize = emojiSizeReal + 2 * 0.2 * fontSizeMultiplier * 14
|
||||
this.emojiSize = fullEmojiSize
|
||||
},
|
||||
showPicker () {
|
||||
showPicker() {
|
||||
this.$refs.popover.showPopover()
|
||||
this.$nextTick(() => {
|
||||
this.onShowing()
|
||||
})
|
||||
},
|
||||
hidePicker () {
|
||||
hidePicker() {
|
||||
this.$refs.popover.hidePopover()
|
||||
},
|
||||
setAnchorEl (el) {
|
||||
setAnchorEl(el) {
|
||||
this.$refs.popover.setAnchorEl(el)
|
||||
},
|
||||
setGroupRef (name) {
|
||||
return el => { this.groupRefs[name] = el }
|
||||
setGroupRef(name) {
|
||||
return (el) => {
|
||||
this.groupRefs[name] = el
|
||||
}
|
||||
},
|
||||
onPopoverShown () {
|
||||
onPopoverShown() {
|
||||
this.$emit('show')
|
||||
},
|
||||
onPopoverClosed () {
|
||||
onPopoverClosed() {
|
||||
this.$emit('close')
|
||||
},
|
||||
onStickerUploaded (e) {
|
||||
onStickerUploaded(e) {
|
||||
this.$emit('sticker-uploaded', e)
|
||||
},
|
||||
onStickerUploadFailed (e) {
|
||||
onStickerUploadFailed(e) {
|
||||
this.$emit('sticker-upload-failed', e)
|
||||
},
|
||||
onEmoji (emoji) {
|
||||
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
|
||||
onEmoji(emoji) {
|
||||
const value = emoji.imageUrl
|
||||
? `:${emoji.displayText}:`
|
||||
: emoji.replacement
|
||||
if (!this.keepOpen) {
|
||||
this.$refs.popover.hidePopover()
|
||||
}
|
||||
this.$emit('emoji', { insertion: value, insertionUrl: emoji.imageUrl, keepOpen: this.keepOpen })
|
||||
this.$emit('emoji', {
|
||||
insertion: value,
|
||||
insertionUrl: emoji.imageUrl,
|
||||
keepOpen: this.keepOpen,
|
||||
})
|
||||
},
|
||||
onScroll (startIndex, endIndex, visibleStartIndex, visibleEndIndex) {
|
||||
onScroll(startIndex, endIndex, visibleStartIndex, visibleEndIndex) {
|
||||
const target = this.$refs['emoji-groups'].$el
|
||||
this.scrolledGroup(target, visibleStartIndex, visibleEndIndex)
|
||||
},
|
||||
scrolledGroup (target, start, end) {
|
||||
scrolledGroup(target, start, end) {
|
||||
const top = target.scrollTop + 5
|
||||
this.$nextTick(() => {
|
||||
this.emojiItems.slice(start, end + 1).forEach(group => {
|
||||
this.emojiItems.slice(start, end + 1).forEach((group) => {
|
||||
const headerId = toHeaderId(group.id)
|
||||
const ref = this.groupRefs['group-' + group.id]
|
||||
if (!ref) { return }
|
||||
if (!ref) {
|
||||
return
|
||||
}
|
||||
const elem = ref.$el.parentElement
|
||||
if (!elem) { return }
|
||||
if (!elem) {
|
||||
return
|
||||
}
|
||||
if (elem && getOffset(elem) <= top) {
|
||||
this.activeGroup = headerId
|
||||
}
|
||||
|
|
@ -220,7 +238,7 @@ const EmojiPicker = {
|
|||
this.scrollHeader()
|
||||
})
|
||||
},
|
||||
scrollHeader () {
|
||||
scrollHeader() {
|
||||
// Scroll the active tab's header into view
|
||||
const headerRef = this.groupRefs['group-header-' + this.activeGroup]
|
||||
const left = headerRef.offsetLeft
|
||||
|
|
@ -228,7 +246,9 @@ const EmojiPicker = {
|
|||
const headerCont = this.$refs.header
|
||||
const currentScroll = headerCont.scrollLeft
|
||||
const currentScrollRight = currentScroll + headerCont.clientWidth
|
||||
const setScroll = s => { headerCont.scrollLeft = s }
|
||||
const setScroll = (s) => {
|
||||
headerCont.scrollLeft = s
|
||||
}
|
||||
|
||||
const margin = 7 // .emoji-tabs-item: padding
|
||||
if (left - margin < currentScroll) {
|
||||
|
|
@ -237,12 +257,12 @@ const EmojiPicker = {
|
|||
setScroll(right + margin - headerCont.clientWidth)
|
||||
}
|
||||
},
|
||||
highlight (groupId) {
|
||||
highlight(groupId) {
|
||||
this.setShowStickers(false)
|
||||
const indexInList = this.emojiItems.findIndex(k => k.id === groupId)
|
||||
const indexInList = this.emojiItems.findIndex((k) => k.id === groupId)
|
||||
this.$refs['emoji-groups'].scrollToItem(indexInList)
|
||||
},
|
||||
updateScrolledClass (target) {
|
||||
updateScrolledClass(target) {
|
||||
if (target.scrollTop <= 5) {
|
||||
this.groupsScrolledClass = 'scrolled-top'
|
||||
} else if (target.scrollTop >= target.scrollTopMax - 5) {
|
||||
|
|
@ -251,16 +271,21 @@ const EmojiPicker = {
|
|||
this.groupsScrolledClass = 'scrolled-middle'
|
||||
}
|
||||
},
|
||||
toggleStickers () {
|
||||
toggleStickers() {
|
||||
this.showingStickers = !this.showingStickers
|
||||
},
|
||||
setShowStickers (value) {
|
||||
setShowStickers(value) {
|
||||
this.showingStickers = value
|
||||
},
|
||||
filterByKeyword (list, keyword) {
|
||||
return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName)
|
||||
filterByKeyword(list, keyword) {
|
||||
return filterByKeyword(
|
||||
list,
|
||||
keyword,
|
||||
this.languages,
|
||||
this.maybeLocalizedEmojiName,
|
||||
)
|
||||
},
|
||||
onShowing () {
|
||||
onShowing() {
|
||||
const oldContentLoaded = this.contentLoaded
|
||||
this.updateEmojiSize()
|
||||
this.recalculateItemPerRow()
|
||||
|
|
@ -277,59 +302,59 @@ const EmojiPicker = {
|
|||
})
|
||||
}
|
||||
},
|
||||
getFilteredEmojiGroups () {
|
||||
getFilteredEmojiGroups() {
|
||||
return this.allEmojiGroups
|
||||
.map(group => ({
|
||||
.map((group) => ({
|
||||
...group,
|
||||
emojis: this.filterByKeyword(group.emojis, trim(this.keyword))
|
||||
emojis: this.filterByKeyword(group.emojis, trim(this.keyword)),
|
||||
}))
|
||||
.filter(group => group.emojis.length > 0)
|
||||
.filter((group) => group.emojis.length > 0)
|
||||
},
|
||||
recalculateItemPerRow () {
|
||||
recalculateItemPerRow() {
|
||||
this.$nextTick(() => {
|
||||
if (!this.$refs['emoji-groups']) {
|
||||
return
|
||||
}
|
||||
this.width = this.$refs['emoji-groups'].$el.clientWidth
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
keyword () {
|
||||
keyword() {
|
||||
this.onScroll()
|
||||
this.debouncedHandleKeywordChange()
|
||||
},
|
||||
allCustomGroups () {
|
||||
allCustomGroups() {
|
||||
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
minItemSize () {
|
||||
minItemSize() {
|
||||
return this.emojiSize
|
||||
},
|
||||
// used to watch it
|
||||
fontSize () {
|
||||
fontSize() {
|
||||
this.$nextTick(() => {
|
||||
this.updateEmojiSize()
|
||||
})
|
||||
return this.$store.getters.mergedConfig.fontSize
|
||||
},
|
||||
emojiHeight () {
|
||||
emojiHeight() {
|
||||
return this.emojiSize
|
||||
},
|
||||
itemPerRow () {
|
||||
itemPerRow() {
|
||||
return this.width ? Math.floor(this.width / this.emojiSize) : 6
|
||||
},
|
||||
activeGroupView () {
|
||||
activeGroupView() {
|
||||
return this.showingStickers ? '' : this.activeGroup
|
||||
},
|
||||
stickersAvailable () {
|
||||
stickersAvailable() {
|
||||
if (this.$store.state.instance.stickers) {
|
||||
return this.$store.state.instance.stickers.length > 0
|
||||
}
|
||||
return 0
|
||||
},
|
||||
allCustomGroups () {
|
||||
allCustomGroups() {
|
||||
if (this.hideCustomEmoji || this.hideCustomEmojiInPicker) {
|
||||
return {}
|
||||
}
|
||||
|
|
@ -339,46 +364,49 @@ const EmojiPicker = {
|
|||
}
|
||||
return emojis
|
||||
},
|
||||
defaultGroup () {
|
||||
defaultGroup() {
|
||||
return Object.keys(this.allCustomGroups)[0]
|
||||
},
|
||||
unicodeEmojiGroups () {
|
||||
return this.$store.getters.standardEmojiGroupList.map(group => ({
|
||||
unicodeEmojiGroups() {
|
||||
return this.$store.getters.standardEmojiGroupList.map((group) => ({
|
||||
id: `standard-${group.id}`,
|
||||
text: this.$t(`emoji.unicode_groups.${group.id}`),
|
||||
icon: UNICODE_EMOJI_GROUP_ICON[group.id],
|
||||
emojis: group.emojis
|
||||
emojis: group.emojis,
|
||||
}))
|
||||
},
|
||||
allEmojiGroups () {
|
||||
allEmojiGroups() {
|
||||
return Object.entries(this.allCustomGroups)
|
||||
.map(([, v]) => v)
|
||||
.concat(this.unicodeEmojiGroups)
|
||||
},
|
||||
stickerPickerEnabled () {
|
||||
stickerPickerEnabled() {
|
||||
return (this.$store.state.instance.stickers || []).length !== 0
|
||||
},
|
||||
debouncedHandleKeywordChange () {
|
||||
debouncedHandleKeywordChange() {
|
||||
return debounce(() => {
|
||||
this.filteredEmojiGroups = this.getFilteredEmojiGroups()
|
||||
}, 500)
|
||||
},
|
||||
emojiItems () {
|
||||
return this.filteredEmojiGroups.map(group =>
|
||||
chunk(group.emojis, this.itemPerRow)
|
||||
.map((items, index) => ({
|
||||
emojiItems() {
|
||||
return this.filteredEmojiGroups
|
||||
.map((group) =>
|
||||
chunk(group.emojis, this.itemPerRow).map((items, index) => ({
|
||||
...group,
|
||||
id: index === 0 ? group.id : `row-${index}-${group.id}`,
|
||||
emojis: items,
|
||||
isFirstRow: index === 0
|
||||
})))
|
||||
isFirstRow: index === 0,
|
||||
})),
|
||||
)
|
||||
.reduce((a, c) => a.concat(c), [])
|
||||
},
|
||||
languages () {
|
||||
return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
|
||||
languages() {
|
||||
return ensureFinalFallback(
|
||||
this.$store.getters.mergedConfig.interfaceLanguage,
|
||||
)
|
||||
},
|
||||
maybeLocalizedEmojiName () {
|
||||
return emoji => {
|
||||
maybeLocalizedEmojiName() {
|
||||
return (emoji) => {
|
||||
if (!emoji.annotations) {
|
||||
return emoji.displayText
|
||||
}
|
||||
|
|
@ -396,10 +424,10 @@ const EmojiPicker = {
|
|||
return emoji.displayText
|
||||
}
|
||||
},
|
||||
isInModal () {
|
||||
isInModal() {
|
||||
return this.popoversZLayer === 'modals'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default EmojiPicker
|
||||
|
|
|
|||
|
|
@ -1,18 +1,11 @@
|
|||
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 StillImage from 'src/components/still-image/still-image.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faPlus,
|
||||
faMinus,
|
||||
faCheck
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faPlus,
|
||||
faMinus,
|
||||
faCheck
|
||||
)
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faCheck, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faPlus, faMinus, faCheck)
|
||||
|
||||
const EMOJI_REACTION_COUNT_CUTOFF = 12
|
||||
|
||||
|
|
@ -21,57 +14,62 @@ const EmojiReactions = {
|
|||
components: {
|
||||
UserAvatar,
|
||||
UserListPopover,
|
||||
StillImage
|
||||
StillImage,
|
||||
},
|
||||
props: ['status'],
|
||||
data: () => ({
|
||||
showAll: false
|
||||
showAll: false,
|
||||
}),
|
||||
computed: {
|
||||
tooManyReactions () {
|
||||
tooManyReactions() {
|
||||
return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF
|
||||
},
|
||||
emojiReactions () {
|
||||
emojiReactions() {
|
||||
return this.showAll
|
||||
? this.status.emoji_reactions
|
||||
: this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF)
|
||||
},
|
||||
showMoreString () {
|
||||
showMoreString() {
|
||||
return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}`
|
||||
},
|
||||
accountsForEmoji () {
|
||||
accountsForEmoji() {
|
||||
return this.status.emoji_reactions.reduce((acc, reaction) => {
|
||||
acc[reaction.name] = reaction.accounts || []
|
||||
return acc
|
||||
}, {})
|
||||
},
|
||||
loggedIn () {
|
||||
loggedIn() {
|
||||
return !!this.$store.state.users.currentUser
|
||||
},
|
||||
remoteInteractionLink () {
|
||||
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
|
||||
}
|
||||
remoteInteractionLink() {
|
||||
return this.$store.getters.remoteInteractionLink({
|
||||
statusId: this.status.id,
|
||||
})
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleShowAll () {
|
||||
toggleShowAll() {
|
||||
this.showAll = !this.showAll
|
||||
},
|
||||
reactedWith (emoji) {
|
||||
return this.status.emoji_reactions.find(r => r.name === emoji).me
|
||||
reactedWith(emoji) {
|
||||
return this.status.emoji_reactions.find((r) => r.name === emoji).me
|
||||
},
|
||||
async fetchEmojiReactionsByIfMissing () {
|
||||
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
|
||||
async fetchEmojiReactionsByIfMissing() {
|
||||
const hasNoAccounts = this.status.emoji_reactions.find((r) => !r.accounts)
|
||||
if (hasNoAccounts) {
|
||||
return await this.$store.dispatch('fetchEmojiReactionsBy', this.status.id)
|
||||
return await this.$store.dispatch(
|
||||
'fetchEmojiReactionsBy',
|
||||
this.status.id,
|
||||
)
|
||||
}
|
||||
},
|
||||
reactWith (emoji) {
|
||||
reactWith(emoji) {
|
||||
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
|
||||
},
|
||||
unreact (emoji) {
|
||||
unreact(emoji) {
|
||||
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
|
||||
},
|
||||
async emojiOnClick (emoji) {
|
||||
async emojiOnClick(emoji) {
|
||||
if (!this.loggedIn) return
|
||||
|
||||
await this.fetchEmojiReactionsByIfMissing()
|
||||
|
|
@ -81,19 +79,23 @@ const EmojiReactions = {
|
|||
this.reactWith(emoji)
|
||||
}
|
||||
},
|
||||
counterTriggerAttrs (reaction) {
|
||||
counterTriggerAttrs(reaction) {
|
||||
return {
|
||||
class: [
|
||||
'emoji-reaction-count-button',
|
||||
{
|
||||
'-picked-reaction': this.reactedWith(reaction.name),
|
||||
toggled: this.reactedWith(reaction.name)
|
||||
}
|
||||
toggled: this.reactedWith(reaction.name),
|
||||
},
|
||||
],
|
||||
'aria-label': this.$t('status.reaction_count_label', { num: reaction.count }, reaction.count)
|
||||
'aria-label': this.$t(
|
||||
'status.reaction_count_label',
|
||||
{ num: reaction.count },
|
||||
reaction.count,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default EmojiReactions
|
||||
|
|
|
|||
|
|
@ -1,45 +1,47 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faCircleNotch
|
||||
)
|
||||
library.add(faCircleNotch)
|
||||
|
||||
const Exporter = {
|
||||
props: {
|
||||
getContent: {
|
||||
type: Function,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
filename: {
|
||||
type: String,
|
||||
default: 'export.csv'
|
||||
default: 'export.csv',
|
||||
},
|
||||
exportButtonLabel: { type: String },
|
||||
processingMessage: { type: String }
|
||||
processingMessage: { type: String },
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
processing: false
|
||||
processing: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
process () {
|
||||
process() {
|
||||
this.processing = true
|
||||
this.getContent()
|
||||
.then((content) => {
|
||||
const fileToDownload = document.createElement('a')
|
||||
fileToDownload.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content))
|
||||
fileToDownload.setAttribute('download', this.filename)
|
||||
fileToDownload.style.display = 'none'
|
||||
document.body.appendChild(fileToDownload)
|
||||
fileToDownload.click()
|
||||
document.body.removeChild(fileToDownload)
|
||||
// Add delay before hiding processing state since browser takes some time to handle file download
|
||||
setTimeout(() => { this.processing = false }, 2000)
|
||||
})
|
||||
}
|
||||
}
|
||||
this.getContent().then((content) => {
|
||||
const fileToDownload = document.createElement('a')
|
||||
fileToDownload.setAttribute(
|
||||
'href',
|
||||
'data:text/plain;charset=utf-8,' + encodeURIComponent(content),
|
||||
)
|
||||
fileToDownload.setAttribute('download', this.filename)
|
||||
fileToDownload.style.display = 'none'
|
||||
document.body.appendChild(fileToDownload)
|
||||
fileToDownload.click()
|
||||
document.body.removeChild(fileToDownload)
|
||||
// Add delay before hiding processing state since browser takes some time to handle file download
|
||||
setTimeout(() => {
|
||||
this.processing = false
|
||||
}, 2000)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default Exporter
|
||||
|
|
|
|||
|
|
@ -1,55 +1,72 @@
|
|||
import { mapGetters } from 'vuex'
|
||||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faUserPlus,
|
||||
faBullhorn,
|
||||
faComments,
|
||||
faBullhorn
|
||||
faUserPlus,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
library.add(
|
||||
faUserPlus,
|
||||
faComments,
|
||||
faBullhorn
|
||||
)
|
||||
library.add(faUserPlus, faComments, faBullhorn)
|
||||
|
||||
const ExtraNotifications = {
|
||||
computed: {
|
||||
shouldShowChats () {
|
||||
return this.mergedConfig.showExtraNotifications && this.mergedConfig.showChatsInExtraNotifications && this.unreadChatCount
|
||||
shouldShowChats() {
|
||||
return (
|
||||
this.mergedConfig.showExtraNotifications &&
|
||||
this.mergedConfig.showChatsInExtraNotifications &&
|
||||
this.unreadChatCount
|
||||
)
|
||||
},
|
||||
shouldShowAnnouncements () {
|
||||
return this.mergedConfig.showExtraNotifications && this.mergedConfig.showAnnouncementsInExtraNotifications && this.unreadAnnouncementCount
|
||||
shouldShowAnnouncements() {
|
||||
return (
|
||||
this.mergedConfig.showExtraNotifications &&
|
||||
this.mergedConfig.showAnnouncementsInExtraNotifications &&
|
||||
this.unreadAnnouncementCount
|
||||
)
|
||||
},
|
||||
shouldShowFollowRequests () {
|
||||
return this.mergedConfig.showExtraNotifications && this.mergedConfig.showFollowRequestsInExtraNotifications && this.followRequestCount
|
||||
shouldShowFollowRequests() {
|
||||
return (
|
||||
this.mergedConfig.showExtraNotifications &&
|
||||
this.mergedConfig.showFollowRequestsInExtraNotifications &&
|
||||
this.followRequestCount
|
||||
)
|
||||
},
|
||||
hasAnythingToShow () {
|
||||
return this.shouldShowChats || this.shouldShowAnnouncements || this.shouldShowFollowRequests
|
||||
hasAnythingToShow() {
|
||||
return (
|
||||
this.shouldShowChats ||
|
||||
this.shouldShowAnnouncements ||
|
||||
this.shouldShowFollowRequests
|
||||
)
|
||||
},
|
||||
shouldShowCustomizationTip () {
|
||||
return this.mergedConfig.showExtraNotificationsTip && this.hasAnythingToShow
|
||||
shouldShowCustomizationTip() {
|
||||
return (
|
||||
this.mergedConfig.showExtraNotificationsTip && this.hasAnythingToShow
|
||||
)
|
||||
},
|
||||
currentUser () {
|
||||
currentUser() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
...mapGetters(['unreadChatCount', 'followRequestCount', 'mergedConfig']),
|
||||
...mapPiniaState(useAnnouncementsStore, {
|
||||
unreadAnnouncementCount: 'unreadAnnouncementCount'
|
||||
})
|
||||
unreadAnnouncementCount: 'unreadAnnouncementCount',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
openNotificationSettings () {
|
||||
openNotificationSettings() {
|
||||
return useInterfaceStore().openSettingsModalTab('notifications')
|
||||
},
|
||||
dismissConfigurationTip () {
|
||||
return this.$store.dispatch('setOption', { name: 'showExtraNotificationsTip', value: false })
|
||||
}
|
||||
}
|
||||
dismissConfigurationTip() {
|
||||
return this.$store.dispatch('setOption', {
|
||||
name: 'showExtraNotificationsTip',
|
||||
value: false,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default ExtraNotifications
|
||||
|
|
|
|||
|
|
@ -2,15 +2,33 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for
|
|||
|
||||
const FeaturesPanel = {
|
||||
computed: {
|
||||
shout: function () { return this.$store.state.instance.shoutAvailable },
|
||||
pleromaChatMessages: function () { return this.$store.state.instance.pleromaChatMessagesAvailable },
|
||||
gopher: function () { return this.$store.state.instance.gopherAvailable },
|
||||
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
|
||||
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
|
||||
minimalScopesMode: function () { return this.$store.state.instance.minimalScopesMode },
|
||||
textlimit: function () { return this.$store.state.instance.textlimit },
|
||||
uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) }
|
||||
}
|
||||
shout: function () {
|
||||
return this.$store.state.instance.shoutAvailable
|
||||
},
|
||||
pleromaChatMessages: function () {
|
||||
return this.$store.state.instance.pleromaChatMessagesAvailable
|
||||
},
|
||||
gopher: function () {
|
||||
return this.$store.state.instance.gopherAvailable
|
||||
},
|
||||
whoToFollow: function () {
|
||||
return this.$store.state.instance.suggestionsEnabled
|
||||
},
|
||||
mediaProxy: function () {
|
||||
return this.$store.state.instance.mediaProxyAvailable
|
||||
},
|
||||
minimalScopesMode: function () {
|
||||
return this.$store.state.instance.minimalScopesMode
|
||||
},
|
||||
textlimit: function () {
|
||||
return this.$store.state.instance.textlimit
|
||||
},
|
||||
uploadlimit: function () {
|
||||
return fileSizeFormatService.fileSizeFormat(
|
||||
this.$store.state.instance.uploadlimit,
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default FeaturesPanel
|
||||
|
|
|
|||
|
|
@ -1,53 +1,54 @@
|
|||
import RuffleService from '../../services/ruffle_service/ruffle_service.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faExclamationTriangle,
|
||||
faStop,
|
||||
faExclamationTriangle
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faStop,
|
||||
faExclamationTriangle
|
||||
)
|
||||
library.add(faStop, faExclamationTriangle)
|
||||
|
||||
const Flash = {
|
||||
props: ['src'],
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
player: false, // can be true, "hidden", false. hidden = element exists
|
||||
loaded: false,
|
||||
ruffleInstance: null
|
||||
ruffleInstance: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openPlayer () {
|
||||
openPlayer() {
|
||||
if (this.player) return // prevent double-loading, or re-loading on failure
|
||||
this.player = 'hidden'
|
||||
RuffleService.getRuffle().then((ruffle) => {
|
||||
const player = ruffle.newest().createPlayer()
|
||||
player.config = {
|
||||
letterbox: 'on'
|
||||
letterbox: 'on',
|
||||
}
|
||||
const container = this.$refs.container
|
||||
container.appendChild(player)
|
||||
player.style.width = '100%'
|
||||
player.style.height = '100%'
|
||||
player.load(this.src).then(() => {
|
||||
this.player = true
|
||||
}).catch((e) => {
|
||||
console.error('Error loading ruffle', e)
|
||||
this.player = 'error'
|
||||
})
|
||||
player
|
||||
.load(this.src)
|
||||
.then(() => {
|
||||
this.player = true
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Error loading ruffle', e)
|
||||
this.player = 'error'
|
||||
})
|
||||
this.ruffleInstance = player
|
||||
this.$emit('playerOpened')
|
||||
})
|
||||
},
|
||||
closePlayer () {
|
||||
closePlayer() {
|
||||
this.ruffleInstance && this.ruffleInstance.remove()
|
||||
this.player = false
|
||||
this.$emit('playerClosed')
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default Flash
|
||||
|
|
|
|||
|
|
@ -1,24 +1,27 @@
|
|||
import {
|
||||
requestFollow,
|
||||
requestUnfollow,
|
||||
} from '../../services/follow_manipulate/follow_manipulate'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||
export default {
|
||||
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
|
||||
components: {
|
||||
ConfirmModal
|
||||
ConfirmModal,
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
inProgress: false,
|
||||
showingConfirmUnfollow: false
|
||||
showingConfirmUnfollow: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
shouldConfirmUnfollow () {
|
||||
shouldConfirmUnfollow() {
|
||||
return this.$store.getters.mergedConfig.modalOnUnfollow
|
||||
},
|
||||
isPressed () {
|
||||
isPressed() {
|
||||
return this.inProgress || this.relationship.following
|
||||
},
|
||||
title () {
|
||||
title() {
|
||||
if (this.inProgress || this.relationship.following) {
|
||||
return this.$t('user_card.follow_unfollow')
|
||||
} else if (this.relationship.requested) {
|
||||
|
|
@ -27,7 +30,7 @@ export default {
|
|||
return this.$t('user_card.follow')
|
||||
}
|
||||
},
|
||||
label () {
|
||||
label() {
|
||||
if (this.inProgress) {
|
||||
return this.$t('user_card.follow_progress')
|
||||
} else if (this.relationship.following) {
|
||||
|
|
@ -38,42 +41,47 @@ export default {
|
|||
return this.$t('user_card.follow')
|
||||
}
|
||||
},
|
||||
disabled () {
|
||||
disabled() {
|
||||
return this.inProgress || this.user.deactivated
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showConfirmUnfollow () {
|
||||
showConfirmUnfollow() {
|
||||
this.showingConfirmUnfollow = true
|
||||
},
|
||||
hideConfirmUnfollow () {
|
||||
hideConfirmUnfollow() {
|
||||
this.showingConfirmUnfollow = false
|
||||
},
|
||||
onClick () {
|
||||
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow()
|
||||
onClick() {
|
||||
this.relationship.following || this.relationship.requested
|
||||
? this.unfollow()
|
||||
: this.follow()
|
||||
},
|
||||
follow () {
|
||||
follow() {
|
||||
this.inProgress = true
|
||||
requestFollow(this.relationship.id, this.$store).then(() => {
|
||||
this.inProgress = false
|
||||
})
|
||||
},
|
||||
unfollow () {
|
||||
unfollow() {
|
||||
if (this.shouldConfirmUnfollow) {
|
||||
this.showConfirmUnfollow()
|
||||
} else {
|
||||
this.doUnfollow()
|
||||
}
|
||||
},
|
||||
doUnfollow () {
|
||||
doUnfollow() {
|
||||
const store = this.$store
|
||||
this.inProgress = true
|
||||
requestUnfollow(this.relationship.id, store).then(() => {
|
||||
this.inProgress = false
|
||||
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id })
|
||||
store.commit('removeStatus', {
|
||||
timeline: 'friends',
|
||||
userId: this.relationship.id,
|
||||
})
|
||||
})
|
||||
|
||||
this.hideConfirmUnfollow()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,27 @@
|
|||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import RemoteFollow from '../remote_follow/remote_follow.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'
|
||||
|
||||
const FollowCard = {
|
||||
props: [
|
||||
'user',
|
||||
'noFollowsYou'
|
||||
],
|
||||
props: ['user', 'noFollowsYou'],
|
||||
components: {
|
||||
BasicUserCard,
|
||||
RemoteFollow,
|
||||
FollowButton,
|
||||
RemoveFollowerButton
|
||||
RemoveFollowerButton,
|
||||
},
|
||||
computed: {
|
||||
isMe () {
|
||||
isMe() {
|
||||
return this.$store.state.users.currentUser.id === this.user.id
|
||||
},
|
||||
loggedIn () {
|
||||
loggedIn() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
relationship () {
|
||||
relationship() {
|
||||
return this.$store.getters.relationship(this.user.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default FollowCard
|
||||
|
|
|
|||
|
|
@ -1,46 +1,48 @@
|
|||
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 { notificationsFromStore } from '../../services/notification_utils/notification_utils.js'
|
||||
|
||||
const FollowRequestCard = {
|
||||
props: ['user'],
|
||||
components: {
|
||||
BasicUserCard,
|
||||
ConfirmModal
|
||||
ConfirmModal,
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
showingApproveConfirmDialog: false,
|
||||
showingDenyConfirmDialog: false
|
||||
showingDenyConfirmDialog: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
findFollowRequestNotificationId () {
|
||||
findFollowRequestNotificationId() {
|
||||
const notif = notificationsFromStore(this.$store).find(
|
||||
(notif) => notif.from_profile.id === this.user.id && notif.type === 'follow_request'
|
||||
(notif) =>
|
||||
notif.from_profile.id === this.user.id &&
|
||||
notif.type === 'follow_request',
|
||||
)
|
||||
return notif && notif.id
|
||||
},
|
||||
showApproveConfirmDialog () {
|
||||
showApproveConfirmDialog() {
|
||||
this.showingApproveConfirmDialog = true
|
||||
},
|
||||
hideApproveConfirmDialog () {
|
||||
hideApproveConfirmDialog() {
|
||||
this.showingApproveConfirmDialog = false
|
||||
},
|
||||
showDenyConfirmDialog () {
|
||||
showDenyConfirmDialog() {
|
||||
this.showingDenyConfirmDialog = true
|
||||
},
|
||||
hideDenyConfirmDialog () {
|
||||
hideDenyConfirmDialog() {
|
||||
this.showingDenyConfirmDialog = false
|
||||
},
|
||||
approveUser () {
|
||||
approveUser() {
|
||||
if (this.shouldConfirmApprove) {
|
||||
this.showApproveConfirmDialog()
|
||||
} else {
|
||||
this.doApprove()
|
||||
}
|
||||
},
|
||||
doApprove () {
|
||||
doApprove() {
|
||||
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
|
||||
|
|
@ -48,40 +50,41 @@ const FollowRequestCard = {
|
|||
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
|
||||
this.$store.dispatch('updateNotification', {
|
||||
id: notifId,
|
||||
updater: notification => {
|
||||
updater: (notification) => {
|
||||
notification.type = 'follow'
|
||||
}
|
||||
},
|
||||
})
|
||||
this.hideApproveConfirmDialog()
|
||||
},
|
||||
denyUser () {
|
||||
denyUser() {
|
||||
if (this.shouldConfirmDeny) {
|
||||
this.showDenyConfirmDialog()
|
||||
} else {
|
||||
this.doDeny()
|
||||
}
|
||||
},
|
||||
doDeny () {
|
||||
doDeny() {
|
||||
const notifId = this.findFollowRequestNotificationId()
|
||||
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
|
||||
this.$store.state.api.backendInteractor
|
||||
.denyUser({ id: this.user.id })
|
||||
.then(() => {
|
||||
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
|
||||
this.$store.dispatch('removeFollowRequest', this.user)
|
||||
})
|
||||
this.hideDenyConfirmDialog()
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
mergedConfig () {
|
||||
mergedConfig() {
|
||||
return this.$store.getters.mergedConfig
|
||||
},
|
||||
shouldConfirmApprove () {
|
||||
shouldConfirmApprove() {
|
||||
return this.mergedConfig.modalOnApproveFollow
|
||||
},
|
||||
shouldConfirmDeny () {
|
||||
shouldConfirmDeny() {
|
||||
return this.mergedConfig.modalOnDenyFollow
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default FollowRequestCard
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ import FollowRequestCard from '../follow_request_card/follow_request_card.vue'
|
|||
|
||||
const FollowRequests = {
|
||||
components: {
|
||||
FollowRequestCard
|
||||
FollowRequestCard,
|
||||
},
|
||||
computed: {
|
||||
requests () {
|
||||
requests() {
|
||||
return this.$store.state.api.followRequests
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default FollowRequests
|
||||
|
|
|
|||
|
|
@ -1,35 +1,29 @@
|
|||
import Select from '../select/select.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import Popover from 'src/components/popover/popover.vue'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import Select from '../select/select.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faExclamationTriangle,
|
||||
faFont,
|
||||
faKeyboard,
|
||||
faFont
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faExclamationTriangle,
|
||||
faKeyboard,
|
||||
faFont
|
||||
)
|
||||
library.add(faExclamationTriangle, faKeyboard, faFont)
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Select,
|
||||
Checkbox,
|
||||
Popover
|
||||
Popover,
|
||||
},
|
||||
props: [
|
||||
'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'
|
||||
],
|
||||
mounted () {
|
||||
props: ['name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'],
|
||||
mounted() {
|
||||
useInterfaceStore().queryLocalFonts()
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
manualEntry: false,
|
||||
availableOptions: [
|
||||
|
|
@ -37,24 +31,24 @@ export default {
|
|||
'serif',
|
||||
'sans-serif',
|
||||
'monospace',
|
||||
...(this.options || [])
|
||||
].filter(_ => _)
|
||||
...(this.options || []),
|
||||
].filter((_) => _),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleManualEntry () {
|
||||
toggleManualEntry() {
|
||||
this.manualEntry = !this.manualEntry
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
present () {
|
||||
present() {
|
||||
return typeof this.modelValue !== 'undefined'
|
||||
},
|
||||
localFontsList () {
|
||||
localFontsList() {
|
||||
return useInterfaceStore().localFonts
|
||||
},
|
||||
localFontsSize () {
|
||||
localFontsSize() {
|
||||
return useInterfaceStore().localFonts?.length
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
|
||||
const FriendsTimeline = {
|
||||
components: {
|
||||
Timeline
|
||||
Timeline,
|
||||
},
|
||||
computed: {
|
||||
timeline () { return this.$store.state.statuses.timelines.friends }
|
||||
}
|
||||
timeline() {
|
||||
return this.$store.state.statuses.timelines.friends
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default FriendsTimeline
|
||||
|
|
|
|||
|
|
@ -4,37 +4,37 @@ export default {
|
|||
virtual: true,
|
||||
variants: {
|
||||
greentext: '.greentext',
|
||||
cyantext: '.cyantext'
|
||||
cyantext: '.cyantext',
|
||||
},
|
||||
states: {
|
||||
faint: '.faint'
|
||||
faint: '.faint',
|
||||
},
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
textColor: '--text',
|
||||
textAuto: 'preserve'
|
||||
}
|
||||
textAuto: 'preserve',
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['faint'],
|
||||
directives: {
|
||||
textOpacity: 0.5
|
||||
}
|
||||
textOpacity: 0.5,
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'greentext',
|
||||
directives: {
|
||||
textColor: '--cGreen',
|
||||
textAuto: 'preserve'
|
||||
}
|
||||
textAuto: 'preserve',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'cyantext',
|
||||
directives: {
|
||||
textColor: '--cBlue',
|
||||
textAuto: 'preserve'
|
||||
}
|
||||
}
|
||||
]
|
||||
textAuto: 'preserve',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { set, sumBy } from 'lodash'
|
||||
|
||||
import { useMediaViewerStore } from 'src/stores/media_viewer'
|
||||
import Attachment from '../attachment/attachment.vue'
|
||||
import { sumBy, set } from 'lodash'
|
||||
|
||||
const Gallery = {
|
||||
props: [
|
||||
|
|
@ -17,52 +18,71 @@ const Gallery = {
|
|||
'shiftUpAttachment',
|
||||
'shiftDnAttachment',
|
||||
'editAttachment',
|
||||
'grid'
|
||||
'grid',
|
||||
],
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
sizes: {},
|
||||
hidingLong: true
|
||||
hidingLong: true,
|
||||
}
|
||||
},
|
||||
components: { Attachment },
|
||||
computed: {
|
||||
rows () {
|
||||
rows() {
|
||||
if (!this.attachments) {
|
||||
return []
|
||||
}
|
||||
const attachments = this.limit > 0
|
||||
? this.attachments.slice(0, this.limit)
|
||||
: this.attachments
|
||||
const attachments =
|
||||
this.limit > 0
|
||||
? this.attachments.slice(0, this.limit)
|
||||
: this.attachments
|
||||
if (this.size === 'hide') {
|
||||
return attachments.map(item => ({ minimal: true, items: [item] }))
|
||||
return attachments.map((item) => ({ minimal: true, items: [item] }))
|
||||
}
|
||||
const rows = this.grid
|
||||
? [{ grid: true, items: attachments }]
|
||||
: attachments.reduce((acc, attachment, i) => {
|
||||
if (attachment.mimetype.includes('audio')) {
|
||||
return [...acc, { audio: true, items: [attachment] }, { items: [] }]
|
||||
}
|
||||
if (!(
|
||||
attachment.mimetype.includes('image') ||
|
||||
attachment.mimetype.includes('video') ||
|
||||
attachment.mimetype.includes('flash')
|
||||
)) {
|
||||
return [...acc, { minimal: true, items: [attachment] }, { items: [] }]
|
||||
}
|
||||
const maxPerRow = 3
|
||||
const attachmentsRemaining = this.attachments.length - i + 1
|
||||
const currentRow = acc[acc.length - 1].items
|
||||
currentRow.push(attachment)
|
||||
if (currentRow.length >= maxPerRow && attachmentsRemaining > maxPerRow) {
|
||||
return [...acc, { items: [] }]
|
||||
} else {
|
||||
return acc
|
||||
}
|
||||
}, [{ items: [] }]).filter(_ => _.items.length > 0)
|
||||
: attachments
|
||||
.reduce(
|
||||
(acc, attachment, i) => {
|
||||
if (attachment.mimetype.includes('audio')) {
|
||||
return [
|
||||
...acc,
|
||||
{ audio: true, items: [attachment] },
|
||||
{ items: [] },
|
||||
]
|
||||
}
|
||||
if (
|
||||
!(
|
||||
attachment.mimetype.includes('image') ||
|
||||
attachment.mimetype.includes('video') ||
|
||||
attachment.mimetype.includes('flash')
|
||||
)
|
||||
) {
|
||||
return [
|
||||
...acc,
|
||||
{ minimal: true, items: [attachment] },
|
||||
{ items: [] },
|
||||
]
|
||||
}
|
||||
const maxPerRow = 3
|
||||
const attachmentsRemaining = this.attachments.length - i + 1
|
||||
const currentRow = acc[acc.length - 1].items
|
||||
currentRow.push(attachment)
|
||||
if (
|
||||
currentRow.length >= maxPerRow &&
|
||||
attachmentsRemaining > maxPerRow
|
||||
) {
|
||||
return [...acc, { items: [] }]
|
||||
} else {
|
||||
return acc
|
||||
}
|
||||
},
|
||||
[{ items: [] }],
|
||||
)
|
||||
.filter((_) => _.items.length > 0)
|
||||
return rows
|
||||
},
|
||||
attachmentsDimensionalScore () {
|
||||
attachmentsDimensionalScore() {
|
||||
return this.rows.reduce((acc, row) => {
|
||||
let size = 0
|
||||
if (row.minimal) {
|
||||
|
|
@ -75,7 +95,7 @@ const Gallery = {
|
|||
return acc + size
|
||||
}, 0)
|
||||
},
|
||||
tooManyAttachments () {
|
||||
tooManyAttachments() {
|
||||
if (this.editable || this.size === 'small') {
|
||||
return false
|
||||
} else if (this.size === 'hide') {
|
||||
|
|
@ -83,38 +103,38 @@ const Gallery = {
|
|||
} else {
|
||||
return this.attachmentsDimensionalScore > 1
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onNaturalSizeLoad ({ id, width, height }) {
|
||||
onNaturalSizeLoad({ id, width, height }) {
|
||||
set(this.sizes, id, { width, height })
|
||||
},
|
||||
rowStyle (row) {
|
||||
rowStyle(row) {
|
||||
if (row.audio) {
|
||||
return { 'padding-bottom': '25%' } // fixed reduced height for audio
|
||||
} else if (!row.minimal && !row.grid) {
|
||||
return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` }
|
||||
return { 'padding-bottom': `${100 / (row.items.length + 0.6)}%` }
|
||||
}
|
||||
},
|
||||
itemStyle (id, row) {
|
||||
const total = sumBy(row, item => this.getAspectRatio(item.id))
|
||||
itemStyle(id, row) {
|
||||
const total = sumBy(row, (item) => this.getAspectRatio(item.id))
|
||||
return { flex: `${this.getAspectRatio(id) / total} 1 0%` }
|
||||
},
|
||||
getAspectRatio (id) {
|
||||
getAspectRatio(id) {
|
||||
const size = this.sizes[id]
|
||||
return size ? size.width / size.height : 1
|
||||
},
|
||||
toggleHidingLong (event) {
|
||||
toggleHidingLong(event) {
|
||||
this.hidingLong = event
|
||||
},
|
||||
openGallery () {
|
||||
openGallery() {
|
||||
useMediaViewerStore().setMedia(this.attachments)
|
||||
useMediaViewerStore().setCurrentMedia(this.attachments[0])
|
||||
},
|
||||
onMedia () {
|
||||
onMedia() {
|
||||
useMediaViewerStore().setMedia(this.attachments)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default Gallery
|
||||
|
|
|
|||
|
|
@ -1,24 +1,21 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
|
||||
library.add(
|
||||
faTimes
|
||||
)
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faTimes)
|
||||
|
||||
const GlobalNoticeList = {
|
||||
computed: {
|
||||
notices () {
|
||||
notices() {
|
||||
return useInterfaceStore().globalNotices
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeNotice (notice) {
|
||||
closeNotice(notice) {
|
||||
useInterfaceStore().removeGlobalNotice(notice)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default GlobalNoticeList
|
||||
|
|
|
|||
|
|
@ -5,20 +5,20 @@ const HashtagLink = {
|
|||
props: {
|
||||
url: {
|
||||
required: true,
|
||||
type: String
|
||||
type: String,
|
||||
},
|
||||
content: {
|
||||
required: true,
|
||||
type: String
|
||||
type: String,
|
||||
},
|
||||
tag: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
onClick() {
|
||||
const tag = this.tag || extractTagFromUrl(this.url)
|
||||
if (tag) {
|
||||
const link = this.generateTagLink(tag)
|
||||
|
|
@ -27,10 +27,10 @@ const HashtagLink = {
|
|||
window.open(this.url, '_blank')
|
||||
}
|
||||
},
|
||||
generateTagLink (tag) {
|
||||
generateTagLink(tag) {
|
||||
return `/tag/${tag}`
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default HashtagLink
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ export default {
|
|||
component: 'Icon',
|
||||
directives: {
|
||||
textColor: '$blend(--stack 0.5 --parent--text)',
|
||||
textAuto: 'no-auto'
|
||||
}
|
||||
}
|
||||
]
|
||||
textAuto: 'no-auto',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,26 @@
|
|||
import 'cropperjs' // This adds all of the cropperjs's components into DOM
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faCircleNotch
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faCircleNotch
|
||||
)
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faCircleNotch)
|
||||
|
||||
const ImageCropper = {
|
||||
props: {
|
||||
// Mime-types to accept, i.e. which filetypes to accept (.gif, .png, etc.)
|
||||
mimes: {
|
||||
type: String,
|
||||
default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon'
|
||||
default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon',
|
||||
},
|
||||
// Fixed aspect-ratio for selection box
|
||||
aspectRatio: {
|
||||
type: Number
|
||||
}
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
dataUrl: undefined,
|
||||
filename: undefined
|
||||
filename: undefined,
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
|
|
@ -31,12 +28,12 @@ const ImageCropper = {
|
|||
'close', // cropper is closed
|
||||
],
|
||||
methods: {
|
||||
destroy () {
|
||||
destroy() {
|
||||
this.$refs.input.value = ''
|
||||
this.dataUrl = undefined
|
||||
this.$emit('close')
|
||||
},
|
||||
submit (cropping = true) {
|
||||
submit(cropping = true) {
|
||||
let cropperPromise
|
||||
if (cropping) {
|
||||
cropperPromise = this.$refs.cropperSelection.$toCanvas()
|
||||
|
|
@ -44,14 +41,14 @@ const ImageCropper = {
|
|||
cropperPromise = Promise.resolve()
|
||||
}
|
||||
|
||||
cropperPromise.then(canvas => {
|
||||
cropperPromise.then((canvas) => {
|
||||
this.$emit('submit', { canvas, file: this.file })
|
||||
})
|
||||
},
|
||||
pickImage () {
|
||||
pickImage() {
|
||||
this.$refs.input.click()
|
||||
},
|
||||
readFile () {
|
||||
readFile() {
|
||||
const fileInput = this.$refs.input
|
||||
if (fileInput.files != null && fileInput.files[0] != null) {
|
||||
this.file = fileInput.files[0]
|
||||
|
|
@ -66,10 +63,10 @@ const ImageCropper = {
|
|||
},
|
||||
inSelection(selection, maxSelection) {
|
||||
return (
|
||||
selection.x >= maxSelection.x
|
||||
&& selection.y >= maxSelection.y
|
||||
&& (selection.x + selection.width) <= (maxSelection.x + maxSelection.width)
|
||||
&& (selection.y + selection.height) <= (maxSelection.y + maxSelection.height)
|
||||
selection.x >= maxSelection.x &&
|
||||
selection.y >= maxSelection.y &&
|
||||
selection.x + selection.width <= maxSelection.x + maxSelection.width &&
|
||||
selection.y + selection.height <= maxSelection.y + maxSelection.height
|
||||
)
|
||||
},
|
||||
onCropperSelectionChange(event) {
|
||||
|
|
@ -84,11 +81,11 @@ const ImageCropper = {
|
|||
}
|
||||
|
||||
if (!this.inSelection(selection, maxSelection)) {
|
||||
event.preventDefault();
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
// listen for input file changes
|
||||
const fileInput = this.$refs.input
|
||||
fileInput.addEventListener('change', this.readFile)
|
||||
|
|
@ -96,7 +93,7 @@ const ImageCropper = {
|
|||
beforeUnmount: function () {
|
||||
const fileInput = this.$refs.input
|
||||
fileInput.removeEventListener('change', this.readFile)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default ImageCropper
|
||||
|
|
|
|||
|
|
@ -1,49 +1,49 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faCircleNotch,
|
||||
faTimes
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faCircleNotch, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faCircleNotch,
|
||||
faTimes
|
||||
)
|
||||
library.add(faCircleNotch, faTimes)
|
||||
|
||||
const Importer = {
|
||||
props: {
|
||||
submitHandler: {
|
||||
type: Function,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
submitButtonLabel: { type: String },
|
||||
successMessage: { type: String },
|
||||
errorMessage: { type: String }
|
||||
errorMessage: { type: String },
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
file: null,
|
||||
error: false,
|
||||
success: false,
|
||||
submitting: false
|
||||
submitting: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change () {
|
||||
change() {
|
||||
this.file = this.$refs.input.files[0]
|
||||
},
|
||||
submit () {
|
||||
submit() {
|
||||
this.dismiss()
|
||||
this.submitting = true
|
||||
this.submitHandler(this.file)
|
||||
.then(() => { this.success = true })
|
||||
.catch(() => { this.error = true })
|
||||
.finally(() => { this.submitting = false })
|
||||
.then(() => {
|
||||
this.success = true
|
||||
})
|
||||
.catch(() => {
|
||||
this.error = true
|
||||
})
|
||||
.finally(() => {
|
||||
this.submitting = false
|
||||
})
|
||||
},
|
||||
dismiss () {
|
||||
dismiss() {
|
||||
this.success = false
|
||||
this.error = false
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default Importer
|
||||
|
|
|
|||
|
|
@ -4,91 +4,96 @@ export default {
|
|||
states: {
|
||||
hover: ':is(:hover, :focus-visible):not(.disabled)',
|
||||
focused: ':focus-within',
|
||||
disabled: '.disabled'
|
||||
disabled: '.disabled',
|
||||
},
|
||||
variants: {
|
||||
checkbox: '.-checkbox',
|
||||
radio: '.-radio'
|
||||
radio: '.-radio',
|
||||
},
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon'
|
||||
],
|
||||
validInnerComponents: ['Text', 'Icon'],
|
||||
defaultRules: [
|
||||
{
|
||||
component: 'Root',
|
||||
directives: {
|
||||
'--defaultInputBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2), $borderSide(#000000 top 0.2), inset 0 0 2 #000000 / 0.15, 1 0 1 1 --text / 0.15, -1 0 1 1 --text / 0.15',
|
||||
'--defaultInputBevel':
|
||||
'shadow | $borderSide(#FFFFFF bottom 0.2), $borderSide(#000000 top 0.2), inset 0 0 2 #000000 / 0.15, 1 0 1 1 --text / 0.15, -1 0 1 1 --text / 0.15',
|
||||
'--defaultInputHoverGlow': 'shadow | 0 0 4 --text / 0.5',
|
||||
'--defaultInputFocusGlow': 'shadow | 0 0 4 4 --link / 0.5'
|
||||
}
|
||||
'--defaultInputFocusGlow': 'shadow | 0 0 4 4 --link / 0.5',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'checkbox',
|
||||
directives: {
|
||||
roundness: 1
|
||||
}
|
||||
roundness: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
directives: {
|
||||
'--font': 'generic | inherit',
|
||||
background: '--fg, -5',
|
||||
roundness: 3,
|
||||
shadow: [{
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 2,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 1
|
||||
}, '--defaultInputBevel']
|
||||
}
|
||||
shadow: [
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
blur: 2,
|
||||
spread: 0,
|
||||
color: '#000000',
|
||||
alpha: 1,
|
||||
},
|
||||
'--defaultInputBevel',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['hover'],
|
||||
directives: {
|
||||
shadow: ['--defaultInputHoverGlow', '--defaultInputBevel']
|
||||
}
|
||||
shadow: ['--defaultInputHoverGlow', '--defaultInputBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['focused'],
|
||||
directives: {
|
||||
shadow: ['--defaultInputFocusGlow', '--defaultInputBevel']
|
||||
}
|
||||
shadow: ['--defaultInputFocusGlow', '--defaultInputBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['focused', 'hover'],
|
||||
directives: {
|
||||
shadow: ['--defaultInputFocusGlow', '--defaultInputHoverGlow', '--defaultInputBevel']
|
||||
}
|
||||
shadow: [
|
||||
'--defaultInputFocusGlow',
|
||||
'--defaultInputHoverGlow',
|
||||
'--defaultInputBevel',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['disabled'],
|
||||
directives: {
|
||||
background: '--parent'
|
||||
}
|
||||
background: '--parent',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Text',
|
||||
parent: {
|
||||
component: 'Input',
|
||||
state: ['disabled']
|
||||
state: ['disabled'],
|
||||
},
|
||||
directives: {
|
||||
textOpacity: 0.25,
|
||||
textOpacityMode: 'blend'
|
||||
}
|
||||
textOpacityMode: 'blend',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'Input',
|
||||
state: ['disabled']
|
||||
state: ['disabled'],
|
||||
},
|
||||
directives: {
|
||||
textOpacity: 0.25,
|
||||
textOpacityMode: 'blend'
|
||||
}
|
||||
}
|
||||
]
|
||||
textOpacityMode: 'blend',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
const InstanceSpecificPanel = {
|
||||
computed: {
|
||||
instanceSpecificPanelContent () {
|
||||
instanceSpecificPanelContent() {
|
||||
return this.$store.state.instance.instanceSpecificPanelContent
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default InstanceSpecificPanel
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Notifications from '../notifications/notifications.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
import Notifications from '../notifications/notifications.vue'
|
||||
|
||||
const tabModeDict = {
|
||||
mentions: ['mention'],
|
||||
|
|
@ -8,26 +8,29 @@ const tabModeDict = {
|
|||
follows: ['follow'],
|
||||
reactions: ['pleroma:emoji_reaction'],
|
||||
reports: ['pleroma:report'],
|
||||
moves: ['move']
|
||||
moves: ['move'],
|
||||
}
|
||||
|
||||
const Interactions = {
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
||||
allowFollowingMove:
|
||||
this.$store.state.users.currentUser.allow_following_move,
|
||||
filterMode: tabModeDict.mentions,
|
||||
canSeeReports: this.$store.state.users.currentUser.privileges.includes('reports_manage_reports')
|
||||
canSeeReports: this.$store.state.users.currentUser.privileges.includes(
|
||||
'reports_manage_reports',
|
||||
),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onModeSwitch (key) {
|
||||
onModeSwitch(key) {
|
||||
this.filterMode = tabModeDict[key]
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Notifications,
|
||||
TabSwitcher
|
||||
}
|
||||
TabSwitcher,
|
||||
},
|
||||
}
|
||||
|
||||
export default Interactions
|
||||
|
|
|
|||
|
|
@ -1,62 +1,63 @@
|
|||
import localeService from '../../services/locale/locale.service.js'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import Select from '../select/select.vue'
|
||||
import ProfileSettingIndicator from 'src/components/settings_modal/helpers/profile_setting_indicator.vue'
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import localeService from '../../services/locale/locale.service.js'
|
||||
import Select from '../select/select.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Select,
|
||||
ProfileSettingIndicator
|
||||
ProfileSettingIndicator,
|
||||
},
|
||||
props: {
|
||||
// List of languages (or just one language)
|
||||
modelValue: {
|
||||
type: [Array, String],
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
// Is this setting stored in user profile (true) or elsewhere (false)
|
||||
// Doesn't affect storage, just shows an icon if true
|
||||
profile: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
computed: {
|
||||
languages () {
|
||||
languages() {
|
||||
return localeService.languages
|
||||
},
|
||||
uniqueId () {
|
||||
return uuidv4()
|
||||
uniqueId() {
|
||||
return uuidv4()
|
||||
},
|
||||
controlledLanguage: {
|
||||
get: function () {
|
||||
return Array.isArray(this.modelValue) ? this.modelValue : [this.modelValue]
|
||||
return Array.isArray(this.modelValue)
|
||||
? this.modelValue
|
||||
: [this.modelValue]
|
||||
},
|
||||
set: function (val) {
|
||||
this.$emit('update:modelValue', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getLanguageName (code) {
|
||||
getLanguageName(code) {
|
||||
return localeService.getLanguageName(code)
|
||||
},
|
||||
addLanguage () {
|
||||
addLanguage() {
|
||||
this.controlledLanguage = [...this.controlledLanguage, '']
|
||||
},
|
||||
setLanguageAt (index, val) {
|
||||
setLanguageAt(index, val) {
|
||||
const lang = [...this.controlledLanguage]
|
||||
lang[index] = val
|
||||
this.controlledLanguage = lang
|
||||
},
|
||||
removeLanguageAt (index) {
|
||||
removeLanguageAt(index) {
|
||||
const lang = [...this.controlledLanguage]
|
||||
lang.splice(index, 1)
|
||||
this.controlledLanguage = lang
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,37 +2,31 @@ import { mapGetters } from 'vuex'
|
|||
|
||||
const LinkPreview = {
|
||||
name: 'LinkPreview',
|
||||
props: [
|
||||
'card',
|
||||
'size',
|
||||
'nsfw'
|
||||
],
|
||||
data () {
|
||||
props: ['card', 'size', 'nsfw'],
|
||||
data() {
|
||||
return {
|
||||
imageLoaded: false
|
||||
imageLoaded: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
useImage () {
|
||||
useImage() {
|
||||
// Currently BE shoudn't give cards if tagged NSFW, this is a bit paranoid
|
||||
// as it makes sure to hide the image if somehow NSFW tagged preview can
|
||||
// exist.
|
||||
return this.card.image && !this.censored && this.size !== 'hide'
|
||||
},
|
||||
censored () {
|
||||
censored() {
|
||||
return this.nsfw && this.hideNsfwConfig
|
||||
},
|
||||
useDescription () {
|
||||
useDescription() {
|
||||
return this.card.description && /\S/.test(this.card.description)
|
||||
},
|
||||
hideNsfwConfig () {
|
||||
hideNsfwConfig() {
|
||||
return this.mergedConfig.hideNsfw
|
||||
},
|
||||
...mapGetters([
|
||||
'mergedConfig'
|
||||
])
|
||||
...mapGetters(['mergedConfig']),
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
if (this.useImage) {
|
||||
const newImg = new Image()
|
||||
newImg.onload = () => {
|
||||
|
|
@ -40,7 +34,7 @@ const LinkPreview = {
|
|||
}
|
||||
newImg.src = this.card.image
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default LinkPreview
|
||||
|
|
|
|||
|
|
@ -3,22 +3,22 @@ export default {
|
|||
selector: 'a',
|
||||
virtual: true,
|
||||
states: {
|
||||
faint: '.faint'
|
||||
faint: '.faint',
|
||||
},
|
||||
defaultRules: [
|
||||
{
|
||||
component: 'Link',
|
||||
directives: {
|
||||
textColor: '--link'
|
||||
}
|
||||
textColor: '--link',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Link',
|
||||
state: ['faint'],
|
||||
directives: {
|
||||
textOpacity: 0.5,
|
||||
textOpacityMode: 'fake'
|
||||
}
|
||||
}
|
||||
]
|
||||
textOpacityMode: 'fake',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,20 +29,20 @@ export default {
|
|||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
default: () => [],
|
||||
},
|
||||
getKey: {
|
||||
type: Function,
|
||||
default: item => item.id
|
||||
default: (item) => item.id,
|
||||
},
|
||||
getClass: {
|
||||
type: Function,
|
||||
default: () => ''
|
||||
default: () => '',
|
||||
},
|
||||
nonInteractive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -2,27 +2,27 @@ import { useListsStore } from 'src/stores/lists'
|
|||
import ListsCard from '../lists_card/lists_card.vue'
|
||||
|
||||
const Lists = {
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
isNew: false
|
||||
isNew: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ListsCard
|
||||
ListsCard,
|
||||
},
|
||||
computed: {
|
||||
lists () {
|
||||
lists() {
|
||||
return useListsStore().allLists
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
cancelNewList () {
|
||||
cancelNewList() {
|
||||
this.isNew = false
|
||||
},
|
||||
newList () {
|
||||
newList() {
|
||||
this.isNew = true
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default Lists
|
||||
|
|
|
|||
|
|
@ -1,16 +1,10 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faEllipsisH
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faEllipsisH
|
||||
)
|
||||
library.add(faEllipsisH)
|
||||
|
||||
const ListsCard = {
|
||||
props: [
|
||||
'list'
|
||||
]
|
||||
props: ['list'],
|
||||
}
|
||||
|
||||
export default ListsCard
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
import { mapState, mapGetters } from 'vuex'
|
||||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import ListsUserSearch from '../lists_user_search/lists_user_search.vue'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faSearch,
|
||||
faChevronLeft
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import { useListsStore } from 'src/stores/lists'
|
||||
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'
|
||||
|
||||
library.add(
|
||||
faSearch,
|
||||
faChevronLeft
|
||||
)
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faSearch, faChevronLeft)
|
||||
|
||||
const ListsNew = {
|
||||
components: {
|
||||
|
|
@ -24,9 +20,9 @@ const ListsNew = {
|
|||
UserAvatar,
|
||||
ListsUserSearch,
|
||||
TabSwitcher,
|
||||
PanelLoading
|
||||
PanelLoading,
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
title: '',
|
||||
titleDraft: '',
|
||||
|
|
@ -35,46 +31,51 @@ const ListsNew = {
|
|||
searchUserIds: [],
|
||||
addedUserIds: new Set([]), // users we added from search, to undo
|
||||
searchLoading: false,
|
||||
reallyDelete: false
|
||||
reallyDelete: false,
|
||||
}
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
if (!this.id) return
|
||||
useListsStore().fetchList({ listId: this.id })
|
||||
useListsStore()
|
||||
.fetchList({ listId: this.id })
|
||||
.then(() => {
|
||||
this.title = this.findListTitle(this.id)
|
||||
this.titleDraft = this.title
|
||||
})
|
||||
useListsStore().fetchListAccounts({ listId: this.id })
|
||||
useListsStore()
|
||||
.fetchListAccounts({ listId: this.id })
|
||||
.then(() => {
|
||||
this.membersUserIds = this.findListAccounts(this.id)
|
||||
this.membersUserIds.forEach(userId => {
|
||||
this.membersUserIds.forEach((userId) => {
|
||||
this.$store.dispatch('fetchUserIfMissing', userId)
|
||||
})
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
id () {
|
||||
id() {
|
||||
return this.$route.params.id
|
||||
},
|
||||
membersUsers () {
|
||||
membersUsers() {
|
||||
return [...this.membersUserIds, ...this.addedUserIds]
|
||||
.map(userId => this.findUser(userId)).filter(user => user)
|
||||
.map((userId) => this.findUser(userId))
|
||||
.filter((user) => user)
|
||||
},
|
||||
searchUsers () {
|
||||
return this.searchUserIds.map(userId => this.findUser(userId)).filter(user => user)
|
||||
searchUsers() {
|
||||
return this.searchUserIds
|
||||
.map((userId) => this.findUser(userId))
|
||||
.filter((user) => user)
|
||||
},
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
}),
|
||||
...mapPiniaState(useListsStore, ['findListTitle', 'findListAccounts']),
|
||||
...mapGetters(['findUser'])
|
||||
...mapGetters(['findUser']),
|
||||
},
|
||||
methods: {
|
||||
onInput () {
|
||||
onInput() {
|
||||
this.search(this.query)
|
||||
},
|
||||
toggleRemoveMember (user) {
|
||||
toggleRemoveMember(user) {
|
||||
if (this.removedUserIds.has(user.id)) {
|
||||
this.id && this.addUser(user)
|
||||
this.removedUserIds.delete(user.id)
|
||||
|
|
@ -83,7 +84,7 @@ const ListsNew = {
|
|||
this.removedUserIds.add(user.id)
|
||||
}
|
||||
},
|
||||
toggleAddFromSearch (user) {
|
||||
toggleAddFromSearch(user) {
|
||||
if (this.addedUserIds.has(user.id)) {
|
||||
this.id && this.removeUser(user.id)
|
||||
this.addedUserIds.delete(user.id)
|
||||
|
|
@ -92,37 +93,41 @@ const ListsNew = {
|
|||
this.addedUserIds.add(user.id)
|
||||
}
|
||||
},
|
||||
isRemoved (user) {
|
||||
isRemoved(user) {
|
||||
return this.removedUserIds.has(user.id)
|
||||
},
|
||||
isAdded (user) {
|
||||
isAdded(user) {
|
||||
return this.addedUserIds.has(user.id)
|
||||
},
|
||||
addUser (user) {
|
||||
addUser(user) {
|
||||
useListsStore().addListAccount({ accountId: user.id, listId: this.id })
|
||||
},
|
||||
removeUser (userId) {
|
||||
removeUser(userId) {
|
||||
useListsStore().removeListAccount({ accountId: userId, listId: this.id })
|
||||
},
|
||||
onSearchLoading () {
|
||||
onSearchLoading() {
|
||||
this.searchLoading = true
|
||||
},
|
||||
onSearchLoadingDone () {
|
||||
onSearchLoadingDone() {
|
||||
this.searchLoading = false
|
||||
},
|
||||
onSearchResults (results) {
|
||||
onSearchResults(results) {
|
||||
this.searchLoading = false
|
||||
this.searchUserIds = results
|
||||
},
|
||||
updateListTitle () {
|
||||
updateListTitle() {
|
||||
useListsStore().setList({ listId: this.id, title: this.titleDraft })
|
||||
this.title = this.findListTitle(this.id)
|
||||
},
|
||||
createList () {
|
||||
useListsStore().createList({ title: this.titleDraft })
|
||||
createList() {
|
||||
useListsStore()
|
||||
.createList({ title: this.titleDraft })
|
||||
.then((list) => {
|
||||
return useListsStore()
|
||||
.setListAccounts({ listId: list.id, accountIds: [...this.addedUserIds] })
|
||||
.setListAccounts({
|
||||
listId: list.id,
|
||||
accountIds: [...this.addedUserIds],
|
||||
})
|
||||
.then(() => list.id)
|
||||
})
|
||||
.then((listId) => {
|
||||
|
|
@ -132,15 +137,15 @@ const ListsNew = {
|
|||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'lists.error',
|
||||
messageArgs: [e.message],
|
||||
level: 'error'
|
||||
level: 'error',
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteList () {
|
||||
deleteList() {
|
||||
useListsStore().deleteList({ listId: this.id })
|
||||
this.$router.push({ name: 'lists' })
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default ListsNew
|
||||
|
|
|
|||
|
|
@ -1,26 +1,25 @@
|
|||
import { mapState } from 'vuex'
|
||||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import { getListEntries } from 'src/components/navigation/filter.js'
|
||||
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||
import { useListsStore } from 'src/stores/lists'
|
||||
|
||||
export const ListsMenuContent = {
|
||||
props: [
|
||||
'showPin'
|
||||
],
|
||||
props: ['showPin'],
|
||||
components: {
|
||||
NavigationEntry
|
||||
NavigationEntry,
|
||||
},
|
||||
computed: {
|
||||
...mapPiniaState(useListsStore, {
|
||||
lists: getListEntries
|
||||
lists: getListEntries,
|
||||
}),
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser,
|
||||
privateMode: state => state.instance.private,
|
||||
federating: state => state.instance.federating
|
||||
})
|
||||
}
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
privateMode: (state) => state.instance.private,
|
||||
federating: (state) => state.instance.federating,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export default ListsMenuContent
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import { useListsStore } from 'src/stores/lists'
|
||||
import Timeline from '../timeline/timeline.vue'
|
||||
|
||||
const ListsTimeline = {
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
listId: null
|
||||
listId: null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Timeline
|
||||
Timeline,
|
||||
},
|
||||
computed: {
|
||||
timeline () { return this.$store.state.statuses.timelines.list }
|
||||
timeline() {
|
||||
return this.$store.state.statuses.timelines.list
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route: function (route) {
|
||||
|
|
@ -19,19 +22,25 @@ const ListsTimeline = {
|
|||
this.$store.dispatch('stopFetchingTimeline', 'list')
|
||||
this.$store.commit('clearTimeline', { timeline: 'list' })
|
||||
useListsStore().fetchList({ listId: this.listId })
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId })
|
||||
this.$store.dispatch('startFetchingTimeline', {
|
||||
timeline: 'list',
|
||||
listId: this.listId,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
this.listId = this.$route.params.id
|
||||
useListsStore().fetchList({ listId: this.listId })
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId })
|
||||
this.$store.dispatch('startFetchingTimeline', {
|
||||
timeline: 'list',
|
||||
listId: this.listId,
|
||||
})
|
||||
},
|
||||
unmounted () {
|
||||
unmounted() {
|
||||
this.$store.dispatch('stopFetchingTimeline', 'list')
|
||||
this.$store.commit('clearTimeline', { timeline: 'list' })
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default ListsTimeline
|
||||
|
|
|
|||
|
|
@ -1,33 +1,29 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faSearch,
|
||||
faChevronLeft
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
|
||||
library.add(
|
||||
faSearch,
|
||||
faChevronLeft
|
||||
)
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faSearch, faChevronLeft)
|
||||
|
||||
const ListsUserSearch = {
|
||||
components: {
|
||||
Checkbox
|
||||
Checkbox,
|
||||
},
|
||||
emits: ['loading', 'loadingDone', 'results'],
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
query: '',
|
||||
followingOnly: true
|
||||
followingOnly: true,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onInput: debounce(function () {
|
||||
this.search(this.query)
|
||||
}, 2000),
|
||||
search (query) {
|
||||
search(query) {
|
||||
if (!query) {
|
||||
this.loading = false
|
||||
return
|
||||
|
|
@ -36,16 +32,25 @@ const ListsUserSearch = {
|
|||
this.loading = true
|
||||
this.$emit('loading')
|
||||
this.userIds = []
|
||||
this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts', following: this.followingOnly })
|
||||
.then(data => {
|
||||
this.$emit('results', data.accounts.map(a => a.id))
|
||||
this.$store
|
||||
.dispatch('search', {
|
||||
q: query,
|
||||
resolve: true,
|
||||
type: 'accounts',
|
||||
following: this.followingOnly,
|
||||
})
|
||||
.then((data) => {
|
||||
this.$emit(
|
||||
'results',
|
||||
data.accounts.map((a) => a.id),
|
||||
)
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
this.$emit('loadingDone')
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default ListsUserSearch
|
||||
|
|
|
|||
|
|
@ -1,56 +1,61 @@
|
|||
import { mapActions, mapState as mapPiniaState, mapStores } from 'pinia'
|
||||
import { mapState } from 'vuex'
|
||||
import { mapStores, mapActions, mapState as mapPiniaState } from 'pinia'
|
||||
import oauthApi from '../../services/new_api/oauth.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faTimes
|
||||
)
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import oauthApi from '../../services/new_api/oauth.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faTimes)
|
||||
|
||||
const LoginForm = {
|
||||
data: () => ({
|
||||
user: {},
|
||||
error: false
|
||||
error: false,
|
||||
}),
|
||||
computed: {
|
||||
isPasswordAuth () { return this.requiredPassword },
|
||||
isTokenAuth () { return this.requiredToken },
|
||||
isPasswordAuth() {
|
||||
return this.requiredPassword
|
||||
},
|
||||
isTokenAuth() {
|
||||
return this.requiredToken
|
||||
},
|
||||
...mapStores(useOAuthStore),
|
||||
...mapState({
|
||||
registrationOpen: state => state.instance.registrationOpen,
|
||||
instance: state => state.instance,
|
||||
loggingIn: state => state.users.loggingIn,
|
||||
registrationOpen: (state) => state.instance.registrationOpen,
|
||||
instance: (state) => state.instance,
|
||||
loggingIn: (state) => state.users.loggingIn,
|
||||
}),
|
||||
...mapPiniaState(useAuthFlowStore, ['requiredPassword', 'requiredToken', 'requiredMFA'])
|
||||
...mapPiniaState(useAuthFlowStore, [
|
||||
'requiredPassword',
|
||||
'requiredToken',
|
||||
'requiredMFA',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useAuthFlowStore, ['requireMFA', 'login']),
|
||||
submit () {
|
||||
submit() {
|
||||
this.isTokenAuth ? this.submitToken() : this.submitPassword()
|
||||
},
|
||||
submitToken () {
|
||||
submitToken() {
|
||||
const data = {
|
||||
instance: this.instance.server,
|
||||
commit: this.$store.commit
|
||||
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.oauthStore.ensureAppToken()
|
||||
.then(() => {
|
||||
const app = {
|
||||
clientId: this.oauthStore.clientId,
|
||||
clientSecret: this.oauthStore.clientSecret,
|
||||
}
|
||||
oauthApi.login({ ...app, ...data })
|
||||
})
|
||||
this.oauthStore.ensureAppToken().then(() => {
|
||||
const app = {
|
||||
clientId: this.oauthStore.clientId,
|
||||
clientSecret: this.oauthStore.clientSecret,
|
||||
}
|
||||
oauthApi.login({ ...app, ...data })
|
||||
})
|
||||
},
|
||||
submitPassword () {
|
||||
submitPassword() {
|
||||
this.error = false
|
||||
|
||||
// NOTE: we do not really need the app token, but obtaining a token and
|
||||
|
|
@ -61,38 +66,43 @@ const LoginForm = {
|
|||
clientSecret: this.oauthStore.clientSecret,
|
||||
}
|
||||
|
||||
oauthApi.getTokenWithCredentials(
|
||||
{
|
||||
oauthApi
|
||||
.getTokenWithCredentials({
|
||||
...app,
|
||||
instance: this.instance.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
|
||||
}
|
||||
this.login(result).then(() => {
|
||||
this.$router.push({ name: 'friends' })
|
||||
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
|
||||
}
|
||||
this.login(result).then(() => {
|
||||
this.$router.push({ name: 'friends' })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
clearError () { this.error = false },
|
||||
focusOnPasswordInput () {
|
||||
clearError() {
|
||||
this.error = false
|
||||
},
|
||||
focusOnPasswordInput() {
|
||||
const passwordInput = this.$refs.passwordInput
|
||||
passwordInput.focus()
|
||||
passwordInput.setSelectionRange(0, passwordInput.value.length)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default LoginForm
|
||||
|
|
|
|||
|
|
@ -1,26 +1,22 @@
|
|||
import StillImage from '../still-image/still-image.vue'
|
||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||
import Flash from 'src/components/flash/flash.vue'
|
||||
import { useMediaViewerStore } from 'src/stores/media_viewer'
|
||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||
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 GestureService from '../../services/gesture_service/gesture_service'
|
||||
import Flash from 'src/components/flash/flash.vue'
|
||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faCircleNotch,
|
||||
faTimes
|
||||
faTimes,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useMediaViewerStore } from 'src/stores/media_viewer'
|
||||
|
||||
library.add(
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faCircleNotch,
|
||||
faTimes
|
||||
)
|
||||
library.add(faChevronLeft, faChevronRight, faCircleNotch, faTimes)
|
||||
|
||||
const MediaModal = {
|
||||
components: {
|
||||
|
|
@ -29,9 +25,9 @@ const MediaModal = {
|
|||
PinchZoom,
|
||||
SwipeClick,
|
||||
Modal,
|
||||
Flash
|
||||
Flash,
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
swipeDirection: GestureService.DIRECTION_LEFT,
|
||||
|
|
@ -40,42 +36,42 @@ const MediaModal = {
|
|||
return window.innerWidth * considerableMoveRatio
|
||||
},
|
||||
pinchZoomMinScale: 1,
|
||||
pinchZoomScaleResetLimit: 1.2
|
||||
pinchZoomScaleResetLimit: 1.2,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showing () {
|
||||
showing() {
|
||||
return useMediaViewerStore().activated
|
||||
},
|
||||
media () {
|
||||
media() {
|
||||
return useMediaViewerStore().media
|
||||
},
|
||||
description () {
|
||||
description() {
|
||||
return this.currentMedia.description
|
||||
},
|
||||
currentIndex () {
|
||||
currentIndex() {
|
||||
return useMediaViewerStore().currentIndex
|
||||
},
|
||||
currentMedia () {
|
||||
currentMedia() {
|
||||
return this.media[this.currentIndex]
|
||||
},
|
||||
canNavigate () {
|
||||
canNavigate() {
|
||||
return this.media.length > 1
|
||||
},
|
||||
type () {
|
||||
type() {
|
||||
return this.currentMedia ? this.getType(this.currentMedia) : null
|
||||
},
|
||||
swipeDisableClickThreshold () {
|
||||
swipeDisableClickThreshold() {
|
||||
// If there is only one media, allow more mouse movements to close the modal
|
||||
// because there is less chance that the user wants to switch to another image
|
||||
return () => this.canNavigate ? 1 : 30
|
||||
}
|
||||
return () => (this.canNavigate ? 1 : 30)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getType (media) {
|
||||
getType(media) {
|
||||
return fileTypeService.fileType(media.mimetype)
|
||||
},
|
||||
hide () {
|
||||
hide() {
|
||||
// HACK: Closing immediately via a touch will cause the click
|
||||
// to be processed on the content below the overlay
|
||||
const transitionTime = 100 // ms
|
||||
|
|
@ -83,7 +79,7 @@ const MediaModal = {
|
|||
useMediaViewerStore().closeMediaViewer()
|
||||
}, transitionTime)
|
||||
},
|
||||
hideIfNotSwiped (event) {
|
||||
hideIfNotSwiped(event) {
|
||||
// If we have swiped over SwipeClick, do not trigger hide
|
||||
const comp = this.$refs.swipeClick
|
||||
if (!comp) {
|
||||
|
|
@ -92,9 +88,12 @@ const MediaModal = {
|
|||
comp.$gesture.click(event)
|
||||
}
|
||||
},
|
||||
goPrev () {
|
||||
goPrev() {
|
||||
if (this.canNavigate) {
|
||||
const prevIndex = this.currentIndex === 0 ? this.media.length - 1 : (this.currentIndex - 1)
|
||||
const prevIndex =
|
||||
this.currentIndex === 0
|
||||
? this.media.length - 1
|
||||
: this.currentIndex - 1
|
||||
const newMedia = this.media[prevIndex]
|
||||
if (this.getType(newMedia) === 'image') {
|
||||
this.loading = true
|
||||
|
|
@ -102,9 +101,12 @@ const MediaModal = {
|
|||
useMediaViewerStore().setCurrentMedia(newMedia)
|
||||
}
|
||||
},
|
||||
goNext () {
|
||||
goNext() {
|
||||
if (this.canNavigate) {
|
||||
const nextIndex = this.currentIndex === this.media.length - 1 ? 0 : (this.currentIndex + 1)
|
||||
const nextIndex =
|
||||
this.currentIndex === this.media.length - 1
|
||||
? 0
|
||||
: this.currentIndex + 1
|
||||
const newMedia = this.media[nextIndex]
|
||||
if (this.getType(newMedia) === 'image') {
|
||||
this.loading = true
|
||||
|
|
@ -112,13 +114,13 @@ const MediaModal = {
|
|||
useMediaViewerStore().setCurrentMedia(newMedia)
|
||||
}
|
||||
},
|
||||
onImageLoaded () {
|
||||
onImageLoaded() {
|
||||
this.loading = false
|
||||
},
|
||||
handleSwipePreview (offsets) {
|
||||
handleSwipePreview(offsets) {
|
||||
this.$refs.pinchZoom.setTransform({ scale: 1, x: offsets[0], y: 0 })
|
||||
},
|
||||
handleSwipeEnd (sign) {
|
||||
handleSwipeEnd(sign) {
|
||||
this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 })
|
||||
if (sign > 0) {
|
||||
this.goNext()
|
||||
|
|
@ -126,33 +128,36 @@ const MediaModal = {
|
|||
this.goPrev()
|
||||
}
|
||||
},
|
||||
handleKeyupEvent (e) {
|
||||
if (this.showing && e.keyCode === 27) { // escape
|
||||
handleKeyupEvent(e) {
|
||||
if (this.showing && e.keyCode === 27) {
|
||||
// escape
|
||||
this.hide()
|
||||
}
|
||||
},
|
||||
handleKeydownEvent (e) {
|
||||
handleKeydownEvent(e) {
|
||||
if (!this.showing) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.keyCode === 39) { // arrow right
|
||||
if (e.keyCode === 39) {
|
||||
// arrow right
|
||||
this.goNext()
|
||||
} else if (e.keyCode === 37) { // arrow left
|
||||
} else if (e.keyCode === 37) {
|
||||
// arrow left
|
||||
this.goPrev()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
window.addEventListener('popstate', this.hide)
|
||||
document.addEventListener('keyup', this.handleKeyupEvent)
|
||||
document.addEventListener('keydown', this.handleKeydownEvent)
|
||||
},
|
||||
unmounted () {
|
||||
unmounted() {
|
||||
window.removeEventListener('popstate', this.hide)
|
||||
document.removeEventListener('keyup', this.handleKeyupEvent)
|
||||
document.removeEventListener('keydown', this.handleKeydownEvent)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default MediaModal
|
||||
|
|
|
|||
|
|
@ -1,34 +1,32 @@
|
|||
/* eslint-env browser */
|
||||
import statusPosterService from '../../services/status_poster/status_poster.service.js'
|
||||
|
||||
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
||||
import statusPosterService from '../../services/status_poster/status_poster.service.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faUpload, faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faCircleNotch, faUpload } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faUpload,
|
||||
faCircleNotch
|
||||
)
|
||||
library.add(faUpload, faCircleNotch)
|
||||
|
||||
const mediaUpload = {
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
uploadCount: 0,
|
||||
uploadReady: true
|
||||
uploadReady: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
uploading () {
|
||||
uploading() {
|
||||
return this.uploadCount > 0
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
onClick() {
|
||||
if (this.uploadReady) {
|
||||
this.$refs.input.click()
|
||||
}
|
||||
},
|
||||
async resizeImage (file) {
|
||||
async resizeImage(file) {
|
||||
// Skip if not an image or if it's a GIF
|
||||
if (!file.type.startsWith('image/') || file.type === 'image/gif') {
|
||||
return file
|
||||
|
|
@ -74,46 +72,67 @@ const mediaUpload = {
|
|||
|
||||
// Check WebP support by trying to create a WebP canvas
|
||||
const testCanvas = document.createElement('canvas')
|
||||
const supportsWebP = testCanvas.toDataURL('image/webp').startsWith('data:image/webp')
|
||||
const supportsWebP = testCanvas
|
||||
.toDataURL('image/webp')
|
||||
.startsWith('data:image/webp')
|
||||
|
||||
// Convert to WebP if supported and alwaysUseJpeg is false, otherwise JPEG
|
||||
const type = (!this.$store.getters.mergedConfig.alwaysUseJpeg && supportsWebP) ? 'image/webp' : 'image/jpeg'
|
||||
const type =
|
||||
!this.$store.getters.mergedConfig.alwaysUseJpeg && supportsWebP
|
||||
? 'image/webp'
|
||||
: 'image/jpeg'
|
||||
const extension = type === 'image/webp' ? '.webp' : '.jpg'
|
||||
|
||||
// Remove the original extension and add new one
|
||||
const newFileName = file.name.replace(/\.[^/.]+$/, '') + extension
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
resolve(new File([blob], newFileName, {
|
||||
type,
|
||||
lastModified: Date.now()
|
||||
}))
|
||||
}, type, 0.85)
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
resolve(
|
||||
new File([blob], newFileName, {
|
||||
type,
|
||||
lastModified: Date.now(),
|
||||
}),
|
||||
)
|
||||
},
|
||||
type,
|
||||
0.85,
|
||||
)
|
||||
}
|
||||
img.src = URL.createObjectURL(file)
|
||||
})
|
||||
},
|
||||
async isAnimatedPng (file) {
|
||||
async isAnimatedPng(file) {
|
||||
const buffer = await file.arrayBuffer()
|
||||
const view = new Uint8Array(buffer)
|
||||
// Look for animated PNG chunks (acTL)
|
||||
for (let i = 0; i < view.length - 8; i++) {
|
||||
if (view[i] === 0x61 && // a
|
||||
view[i + 1] === 0x63 && // c
|
||||
view[i + 2] === 0x54 && // T
|
||||
view[i + 3] === 0x4C) { // L
|
||||
if (
|
||||
view[i] === 0x61 && // a
|
||||
view[i + 1] === 0x63 && // c
|
||||
view[i + 2] === 0x54 && // T
|
||||
view[i + 3] === 0x4c
|
||||
) {
|
||||
// L
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
async uploadFile (file) {
|
||||
async uploadFile(file) {
|
||||
const self = this
|
||||
const store = this.$store
|
||||
if (file.size > store.state.instance.uploadlimit) {
|
||||
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
|
||||
const allowedsize = fileSizeFormatService.fileSizeFormat(store.state.instance.uploadlimit)
|
||||
self.$emit('upload-failed', 'file_too_big', { filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit })
|
||||
const allowedsize = fileSizeFormatService.fileSizeFormat(
|
||||
store.state.instance.uploadlimit,
|
||||
)
|
||||
self.$emit('upload-failed', 'file_too_big', {
|
||||
filesize: filesize.num,
|
||||
filesizeunit: filesize.unit,
|
||||
allowedsize: allowedsize.num,
|
||||
allowedsizeunit: allowedsize.unit,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -125,36 +144,38 @@ const mediaUpload = {
|
|||
self.$emit('uploading')
|
||||
self.uploadCount++
|
||||
|
||||
statusPosterService.uploadMedia({ store, formData })
|
||||
.then((fileData) => {
|
||||
statusPosterService.uploadMedia({ store, formData }).then(
|
||||
(fileData) => {
|
||||
self.$emit('uploaded', fileData)
|
||||
self.decreaseUploadCount()
|
||||
}, (error) => {
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error uploading file', error)
|
||||
self.$emit('upload-failed', 'default')
|
||||
self.decreaseUploadCount()
|
||||
})
|
||||
},
|
||||
)
|
||||
},
|
||||
decreaseUploadCount () {
|
||||
decreaseUploadCount() {
|
||||
this.uploadCount--
|
||||
if (this.uploadCount === 0) {
|
||||
this.$emit('all-uploaded')
|
||||
}
|
||||
},
|
||||
clearFile () {
|
||||
clearFile() {
|
||||
this.uploadReady = false
|
||||
this.$nextTick(() => {
|
||||
this.uploadReady = true
|
||||
})
|
||||
},
|
||||
multiUpload (files) {
|
||||
multiUpload(files) {
|
||||
for (const file of files) {
|
||||
this.uploadFile(file)
|
||||
}
|
||||
},
|
||||
change ({ target }) {
|
||||
change({ target }) {
|
||||
this.multiUpload(target.files)
|
||||
}
|
||||
},
|
||||
},
|
||||
props: {
|
||||
dropFiles: Object,
|
||||
|
|
@ -162,16 +183,16 @@ const mediaUpload = {
|
|||
normalButton: Boolean,
|
||||
acceptTypes: {
|
||||
type: String,
|
||||
default: '*/*'
|
||||
}
|
||||
default: '*/*',
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
dropFiles: function (fileInfos) {
|
||||
if (!this.uploading) {
|
||||
this.multiUpload(fileInfos)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default mediaUpload
|
||||
|
|
|
|||
|
|
@ -1,155 +1,165 @@
|
|||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faAt
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
library.add(
|
||||
faAt
|
||||
)
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
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 { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faAt } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faAt)
|
||||
|
||||
const MentionLink = {
|
||||
name: 'MentionLink',
|
||||
components: {
|
||||
UserAvatar,
|
||||
UnicodeDomainIndicator,
|
||||
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
|
||||
UserPopover: defineAsyncComponent(
|
||||
() => import('../user_popover/user_popover.vue'),
|
||||
),
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
required: true,
|
||||
type: String
|
||||
type: String,
|
||||
},
|
||||
content: {
|
||||
required: true,
|
||||
type: String
|
||||
type: String,
|
||||
},
|
||||
userId: {
|
||||
required: false,
|
||||
type: String
|
||||
type: String,
|
||||
},
|
||||
userScreenName: {
|
||||
required: false,
|
||||
type: String
|
||||
}
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
hasSelection: false
|
||||
hasSelection: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
onClick() {
|
||||
if (this.shouldShowTooltip) return
|
||||
const link = generateProfileLink(
|
||||
this.userId || this.user.id,
|
||||
this.userScreenName || this.user.screen_name
|
||||
this.userScreenName || this.user.screen_name,
|
||||
)
|
||||
this.$router.push(link)
|
||||
},
|
||||
handleSelection () {
|
||||
handleSelection() {
|
||||
if (this.$refs.full) {
|
||||
this.hasSelection = document.getSelection().containsNode(this.$refs.full, true)
|
||||
this.hasSelection = document
|
||||
.getSelection()
|
||||
.containsNode(this.$refs.full, true)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
document.addEventListener('selectionchange', this.handleSelection)
|
||||
},
|
||||
unmounted () {
|
||||
unmounted() {
|
||||
document.removeEventListener('selectionchange', this.handleSelection)
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.url && this.$store && this.$store.getters.findUserByUrl(this.url)
|
||||
user() {
|
||||
return (
|
||||
this.url && this.$store && this.$store.getters.findUserByUrl(this.url)
|
||||
)
|
||||
},
|
||||
isYou () {
|
||||
isYou() {
|
||||
// FIXME why user !== currentUser???
|
||||
return this.user && this.user.id === this.currentUser.id
|
||||
},
|
||||
userName () {
|
||||
userName() {
|
||||
return this.user && this.userNameFullUi.split('@')[0]
|
||||
},
|
||||
serverName () {
|
||||
serverName() {
|
||||
// XXX assumed that domain does not contain @
|
||||
return this.user && (this.userNameFullUi.split('@')[1] || this.$store.getters.instanceDomain)
|
||||
return (
|
||||
this.user &&
|
||||
(this.userNameFullUi.split('@')[1] ||
|
||||
this.$store.getters.instanceDomain)
|
||||
)
|
||||
},
|
||||
userNameFull () {
|
||||
userNameFull() {
|
||||
return this.user && this.user.screen_name
|
||||
},
|
||||
userNameFullUi () {
|
||||
userNameFullUi() {
|
||||
return this.user && this.user.screen_name_ui
|
||||
},
|
||||
highlight () {
|
||||
highlight() {
|
||||
return this.user && this.mergedConfig.highlight[this.user.screen_name]
|
||||
},
|
||||
highlightType () {
|
||||
return this.highlight && ('-' + this.highlight.type)
|
||||
highlightType() {
|
||||
return this.highlight && '-' + this.highlight.type
|
||||
},
|
||||
highlightClass () {
|
||||
highlightClass() {
|
||||
if (this.highlight) return highlightClass(this.user)
|
||||
},
|
||||
style () {
|
||||
style() {
|
||||
if (this.highlight) {
|
||||
/* eslint-disable no-unused-vars */
|
||||
const {
|
||||
backgroundColor,
|
||||
backgroundPosition,
|
||||
backgroundImage,
|
||||
...rest
|
||||
} = highlightStyle(this.highlight)
|
||||
/* eslint-enable no-unused-vars */
|
||||
return rest
|
||||
}
|
||||
},
|
||||
classnames () {
|
||||
classnames() {
|
||||
return [
|
||||
{
|
||||
'-you': this.isYou && this.shouldBoldenYou,
|
||||
'-highlighted': this.highlight,
|
||||
'-has-selection': this.hasSelection
|
||||
'-has-selection': this.hasSelection,
|
||||
},
|
||||
this.highlightType
|
||||
this.highlightType,
|
||||
]
|
||||
},
|
||||
isRemote () {
|
||||
isRemote() {
|
||||
return this.userName !== this.userNameFull
|
||||
},
|
||||
shouldShowFullUserName () {
|
||||
shouldShowFullUserName() {
|
||||
const conf = this.mergedConfig.mentionLinkDisplay
|
||||
if (conf === 'short') {
|
||||
return false
|
||||
} else if (conf === 'full') {
|
||||
return true
|
||||
} else { // full_for_remote
|
||||
} else {
|
||||
// full_for_remote
|
||||
return this.isRemote
|
||||
}
|
||||
},
|
||||
shouldShowTooltip () {
|
||||
shouldShowTooltip() {
|
||||
return this.mergedConfig.mentionLinkShowTooltip
|
||||
},
|
||||
shouldShowAvatar () {
|
||||
shouldShowAvatar() {
|
||||
return this.mergedConfig.mentionLinkShowAvatar
|
||||
},
|
||||
shouldShowYous () {
|
||||
shouldShowYous() {
|
||||
return this.mergedConfig.mentionLinkShowYous
|
||||
},
|
||||
shouldBoldenYou () {
|
||||
shouldBoldenYou() {
|
||||
return this.mergedConfig.mentionLinkBoldenYou
|
||||
},
|
||||
shouldFadeDomain () {
|
||||
shouldFadeDomain() {
|
||||
return this.mergedConfig.mentionLinkFadeDomain
|
||||
},
|
||||
...mapGetters(['mergedConfig']),
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
})
|
||||
}
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export default MentionLink
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ import Timeline from '../timeline/timeline.vue'
|
|||
|
||||
const Mentions = {
|
||||
computed: {
|
||||
timeline () {
|
||||
timeline() {
|
||||
return this.$store.state.statuses.timelines.mentions
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Timeline
|
||||
}
|
||||
Timeline,
|
||||
},
|
||||
}
|
||||
|
||||
export default Mentions
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import MentionLink from 'src/components/mention_link/mention_link.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import MentionLink from 'src/components/mention_link/mention_link.vue'
|
||||
|
||||
export const MENTIONS_LIMIT = 5
|
||||
|
||||
const MentionsLine = {
|
||||
|
|
@ -8,30 +9,30 @@ const MentionsLine = {
|
|||
props: {
|
||||
mentions: {
|
||||
required: true,
|
||||
type: Array
|
||||
}
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
data: () => ({ expanded: false }),
|
||||
components: {
|
||||
MentionLink
|
||||
MentionLink,
|
||||
},
|
||||
computed: {
|
||||
mentionsComputed () {
|
||||
mentionsComputed() {
|
||||
return this.mentions.slice(0, MENTIONS_LIMIT)
|
||||
},
|
||||
extraMentions () {
|
||||
extraMentions() {
|
||||
return this.mentions.slice(MENTIONS_LIMIT)
|
||||
},
|
||||
manyMentions () {
|
||||
manyMentions() {
|
||||
return this.extraMentions.length > 0
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
...mapGetters(['mergedConfig']),
|
||||
},
|
||||
methods: {
|
||||
toggleShowMore () {
|
||||
toggleShowMore() {
|
||||
this.expanded = !this.expanded
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default MentionsLine
|
||||
|
|
|
|||
|
|
@ -1,109 +1,105 @@
|
|||
export default {
|
||||
name: 'MenuItem',
|
||||
selector: '.menu-item',
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon',
|
||||
'Border'
|
||||
],
|
||||
validInnerComponents: ['Text', 'Icon', 'Border'],
|
||||
states: {
|
||||
hover: ':is(:hover, :focus-visible, :has(:focus-visible)):not(.disabled)',
|
||||
active: '.-active',
|
||||
disabled: '.disabled'
|
||||
disabled: '.disabled',
|
||||
},
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
background: '--bg',
|
||||
opacity: 0
|
||||
}
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['hover'],
|
||||
directives: {
|
||||
background: '$mod(--bg 5)',
|
||||
opacity: 1
|
||||
}
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['active'],
|
||||
directives: {
|
||||
background: '$mod(--bg 10)',
|
||||
opacity: 1
|
||||
}
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['active', 'hover'],
|
||||
directives: {
|
||||
background: '$mod(--bg 15)',
|
||||
opacity: 1
|
||||
}
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Text',
|
||||
parent: {
|
||||
component: 'MenuItem',
|
||||
state: ['hover']
|
||||
state: ['hover'],
|
||||
},
|
||||
directives: {
|
||||
textColor: '--link',
|
||||
textAuto: 'no-preserve'
|
||||
}
|
||||
textAuto: 'no-preserve',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Text',
|
||||
parent: {
|
||||
component: 'MenuItem',
|
||||
state: ['active']
|
||||
state: ['active'],
|
||||
},
|
||||
directives: {
|
||||
textColor: '--link',
|
||||
textAuto: 'no-preserve'
|
||||
}
|
||||
textAuto: 'no-preserve',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'MenuItem',
|
||||
state: ['active']
|
||||
state: ['active'],
|
||||
},
|
||||
directives: {
|
||||
textColor: '--link',
|
||||
textAuto: 'no-preserve'
|
||||
}
|
||||
textAuto: 'no-preserve',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'MenuItem',
|
||||
state: ['hover']
|
||||
state: ['hover'],
|
||||
},
|
||||
directives: {
|
||||
textColor: '--link',
|
||||
textAuto: 'no-preserve'
|
||||
}
|
||||
textAuto: 'no-preserve',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Text',
|
||||
parent: {
|
||||
component: 'MenuItem',
|
||||
state: ['disabled']
|
||||
state: ['disabled'],
|
||||
},
|
||||
directives: {
|
||||
textOpacity: 0.25,
|
||||
textOpacityMode: 'blend'
|
||||
}
|
||||
textOpacityMode: 'blend',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'MenuItem',
|
||||
state: ['disabled']
|
||||
state: ['disabled'],
|
||||
},
|
||||
directives: {
|
||||
textOpacity: 0.25,
|
||||
textOpacityMode: 'blend'
|
||||
}
|
||||
}
|
||||
]
|
||||
textOpacityMode: 'blend',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,42 @@
|
|||
import mfaApi from '../../services/new_api/mfa.js'
|
||||
import { mapActions, mapState as mapPiniaState, mapStores } from 'pinia'
|
||||
import { mapState } from 'vuex'
|
||||
import { mapStores, mapActions, mapState as mapPiniaState } from 'pinia'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faTimes
|
||||
)
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import mfaApi from '../../services/new_api/mfa.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faTimes)
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
code: null,
|
||||
error: false
|
||||
error: false,
|
||||
}),
|
||||
computed: {
|
||||
...mapPiniaState(useAuthFlowStore, {
|
||||
authSettings: store => store.settings
|
||||
authSettings: (store) => store.settings,
|
||||
}),
|
||||
...mapStores(useOAuthStore),
|
||||
...mapState({
|
||||
instance: 'instance',
|
||||
})
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useAuthFlowStore, ['requireTOTP', 'abortMFA', 'login']),
|
||||
clearError () { this.error = false },
|
||||
clearError() {
|
||||
this.error = false
|
||||
},
|
||||
|
||||
focusOnCodeInput () {
|
||||
focusOnCodeInput() {
|
||||
const codeInput = this.$refs.codeInput
|
||||
codeInput.focus()
|
||||
codeInput.setSelectionRange(0, codeInput.value.length)
|
||||
},
|
||||
|
||||
submit () {
|
||||
submit() {
|
||||
const { clientId, clientSecret } = this.oauthStore
|
||||
|
||||
const data = {
|
||||
|
|
@ -44,7 +44,7 @@ export default {
|
|||
clientSecret,
|
||||
instance: this.instance.server,
|
||||
mfaToken: this.authSettings.mfa_token,
|
||||
code: this.code
|
||||
code: this.code,
|
||||
}
|
||||
|
||||
mfaApi.verifyRecoveryCode(data).then((result) => {
|
||||
|
|
@ -59,6 +59,6 @@ export default {
|
|||
this.$router.push({ name: 'friends' })
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,42 @@
|
|||
import mfaApi from '../../services/new_api/mfa.js'
|
||||
import { mapActions, mapState as mapPiniaState, mapStores } from 'pinia'
|
||||
import { mapState } from 'vuex'
|
||||
import { mapStores, mapActions, mapState as mapPiniaState } from 'pinia'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faTimes
|
||||
)
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import mfaApi from '../../services/new_api/mfa.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faTimes)
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
code: null,
|
||||
error: false
|
||||
error: false,
|
||||
}),
|
||||
computed: {
|
||||
...mapPiniaState(useAuthFlowStore, {
|
||||
authSettings: store => store.settings
|
||||
authSettings: (store) => store.settings,
|
||||
}),
|
||||
...mapStores(useOAuthStore),
|
||||
...mapState({
|
||||
instance: 'instance',
|
||||
})
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useAuthFlowStore, ['requireRecovery', 'abortMFA', 'login']),
|
||||
clearError () { this.error = false },
|
||||
clearError() {
|
||||
this.error = false
|
||||
},
|
||||
|
||||
focusOnCodeInput () {
|
||||
focusOnCodeInput() {
|
||||
const codeInput = this.$refs.codeInput
|
||||
codeInput.focus()
|
||||
codeInput.setSelectionRange(0, codeInput.value.length)
|
||||
},
|
||||
|
||||
submit () {
|
||||
submit() {
|
||||
const { clientId, clientSecret } = this.oauthStore
|
||||
|
||||
const data = {
|
||||
|
|
@ -44,7 +44,7 @@ export default {
|
|||
clientSecret,
|
||||
instance: this.instance.server,
|
||||
mfaToken: this.authSettings.mfa_token,
|
||||
code: this.code
|
||||
code: this.code,
|
||||
}
|
||||
|
||||
mfaApi.verifyOTPCode(data).then((result) => {
|
||||
|
|
@ -59,6 +59,6 @@ export default {
|
|||
this.$router.push({ name: 'friends' })
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
export default {
|
||||
name: 'MobileDrawer',
|
||||
selector: '.mobile-drawer',
|
||||
validInnerComponents: [
|
||||
'MenuItem'
|
||||
],
|
||||
validInnerComponents: ['MenuItem'],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
background: '--bg',
|
||||
backgroundNoCssColor: 'yes'
|
||||
}
|
||||
}
|
||||
]
|
||||
backgroundNoCssColor: 'yes',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,99 +1,98 @@
|
|||
import SideDrawer from '../side_drawer/side_drawer.vue'
|
||||
import Notifications from '../notifications/notifications.vue'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import GestureService from '../../services/gesture_service/gesture_service'
|
||||
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||
|
||||
import {
|
||||
unseenNotificationsFromStore,
|
||||
countExtraNotifications
|
||||
} from '../../services/notification_utils/notification_utils'
|
||||
|
||||
import { mapGetters } from 'vuex'
|
||||
import { mapState } from 'pinia'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
|
||||
import GestureService from '../../services/gesture_service/gesture_service'
|
||||
import {
|
||||
countExtraNotifications,
|
||||
unseenNotificationsFromStore,
|
||||
} from '../../services/notification_utils/notification_utils'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import Notifications from '../notifications/notifications.vue'
|
||||
import SideDrawer from '../side_drawer/side_drawer.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes,
|
||||
faBell,
|
||||
faBars,
|
||||
faArrowUp,
|
||||
faBars,
|
||||
faBell,
|
||||
faCheckDouble,
|
||||
faMinus,
|
||||
faCheckDouble
|
||||
faTimes,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faTimes,
|
||||
faBell,
|
||||
faBars,
|
||||
faArrowUp,
|
||||
faMinus,
|
||||
faCheckDouble
|
||||
)
|
||||
library.add(faTimes, faBell, faBars, faArrowUp, faMinus, faCheckDouble)
|
||||
|
||||
const MobileNav = {
|
||||
components: {
|
||||
SideDrawer,
|
||||
Notifications,
|
||||
NavigationPins,
|
||||
ConfirmModal
|
||||
ConfirmModal,
|
||||
},
|
||||
data: () => ({
|
||||
notificationsCloseGesture: undefined,
|
||||
notificationsOpen: false,
|
||||
notificationsAtTop: true,
|
||||
showingConfirmLogout: false
|
||||
showingConfirmLogout: false,
|
||||
}),
|
||||
created () {
|
||||
created() {
|
||||
this.notificationsCloseGesture = GestureService.swipeGesture(
|
||||
GestureService.DIRECTION_RIGHT,
|
||||
() => this.closeMobileNotifications(true),
|
||||
50
|
||||
50,
|
||||
)
|
||||
},
|
||||
computed: {
|
||||
currentUser () {
|
||||
currentUser() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
unseenNotifications () {
|
||||
unseenNotifications() {
|
||||
return unseenNotificationsFromStore(this.$store)
|
||||
},
|
||||
unseenNotificationsCount () {
|
||||
return this.unseenNotifications.length + countExtraNotifications(this.$store)
|
||||
unseenNotificationsCount() {
|
||||
return (
|
||||
this.unseenNotifications.length + countExtraNotifications(this.$store)
|
||||
)
|
||||
},
|
||||
unseenCount () {
|
||||
unseenCount() {
|
||||
return this.unseenNotifications.length
|
||||
},
|
||||
unseenCountBadgeText () {
|
||||
unseenCountBadgeText() {
|
||||
return `${this.unseenCount ? this.unseenCount : ''}`
|
||||
},
|
||||
hideSitename () { return this.$store.state.instance.hideSitename },
|
||||
sitename () { return this.$store.state.instance.name },
|
||||
isChat () {
|
||||
hideSitename() {
|
||||
return this.$store.state.instance.hideSitename
|
||||
},
|
||||
sitename() {
|
||||
return this.$store.state.instance.name
|
||||
},
|
||||
isChat() {
|
||||
return this.$route.name === 'chat'
|
||||
},
|
||||
...mapState(useAnnouncementsStore, ['unreadAnnouncementCount']),
|
||||
...mapState(useServerSideStorageStore, {
|
||||
pinnedItems: store => new Set(store.prefsStorage.collections.pinnedNavItems).has('chats')
|
||||
pinnedItems: (store) =>
|
||||
new Set(store.prefsStorage.collections.pinnedNavItems).has('chats'),
|
||||
}),
|
||||
shouldConfirmLogout () {
|
||||
shouldConfirmLogout() {
|
||||
return this.$store.getters.mergedConfig.modalOnLogout
|
||||
},
|
||||
closingDrawerMarksAsSeen () {
|
||||
closingDrawerMarksAsSeen() {
|
||||
return this.$store.getters.mergedConfig.closingDrawerMarksAsSeen
|
||||
},
|
||||
...mapGetters(['unreadChatCount'])
|
||||
...mapGetters(['unreadChatCount']),
|
||||
},
|
||||
methods: {
|
||||
toggleMobileSidebar () {
|
||||
toggleMobileSidebar() {
|
||||
this.$refs.sideDrawer.toggleDrawer()
|
||||
},
|
||||
openMobileNotifications () {
|
||||
openMobileNotifications() {
|
||||
this.notificationsOpen = true
|
||||
},
|
||||
closeMobileNotifications (markRead) {
|
||||
closeMobileNotifications(markRead) {
|
||||
if (this.notificationsOpen) {
|
||||
// make sure to mark notifs seen only when the notifs were open and not
|
||||
// from close-calls.
|
||||
|
|
@ -103,53 +102,53 @@ const MobileNav = {
|
|||
}
|
||||
}
|
||||
},
|
||||
notificationsTouchStart (e) {
|
||||
notificationsTouchStart(e) {
|
||||
GestureService.beginSwipe(e, this.notificationsCloseGesture)
|
||||
},
|
||||
notificationsTouchMove (e) {
|
||||
notificationsTouchMove(e) {
|
||||
GestureService.updateSwipe(e, this.notificationsCloseGesture)
|
||||
},
|
||||
scrollToTop () {
|
||||
scrollToTop() {
|
||||
window.scrollTo(0, 0)
|
||||
},
|
||||
scrollMobileNotificationsToTop () {
|
||||
scrollMobileNotificationsToTop() {
|
||||
this.$refs.mobileNotifications.scrollTo(0, 0)
|
||||
},
|
||||
showConfirmLogout () {
|
||||
showConfirmLogout() {
|
||||
this.showingConfirmLogout = true
|
||||
},
|
||||
hideConfirmLogout () {
|
||||
hideConfirmLogout() {
|
||||
this.showingConfirmLogout = false
|
||||
},
|
||||
logout () {
|
||||
logout() {
|
||||
if (!this.shouldConfirmLogout) {
|
||||
this.doLogout()
|
||||
} else {
|
||||
this.showConfirmLogout()
|
||||
}
|
||||
},
|
||||
doLogout () {
|
||||
doLogout() {
|
||||
this.$router.replace('/main/public')
|
||||
this.$store.dispatch('logout')
|
||||
this.hideConfirmLogout()
|
||||
},
|
||||
markNotificationsAsSeen () {
|
||||
markNotificationsAsSeen() {
|
||||
this.$store.dispatch('markNotificationsAsSeen')
|
||||
},
|
||||
onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) {
|
||||
onScroll({ target: { scrollTop, clientHeight, scrollHeight } }) {
|
||||
this.notificationsAtTop = scrollTop > 0
|
||||
if (scrollTop + clientHeight >= scrollHeight) {
|
||||
this.$refs.notifications.fetchOlderNotifications()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route () {
|
||||
$route() {
|
||||
// handles closing notificaitons when you press any router-link on the
|
||||
// notifications.
|
||||
this.closeMobileNotifications()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default MobileNav
|
||||
|
|
|
|||
|
|
@ -1,57 +1,55 @@
|
|||
import { debounce } from 'lodash'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faPen
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { usePostStatusStore } from 'src/stores/post_status'
|
||||
|
||||
library.add(
|
||||
faPen
|
||||
)
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faPen } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const HIDDEN_FOR_PAGES = new Set([
|
||||
'chats',
|
||||
'chat',
|
||||
'lists-edit'
|
||||
])
|
||||
library.add(faPen)
|
||||
|
||||
const HIDDEN_FOR_PAGES = new Set(['chats', 'chat', 'lists-edit'])
|
||||
|
||||
const MobilePostStatusButton = {
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
hidden: false,
|
||||
scrollingDown: false,
|
||||
inputActive: false,
|
||||
oldScrollPos: 0,
|
||||
amountScrolled: 0
|
||||
amountScrolled: 0,
|
||||
}
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
if (this.autohideFloatingPostButton) {
|
||||
this.activateFloatingPostButtonAutohide()
|
||||
}
|
||||
window.addEventListener('resize', this.handleOSK)
|
||||
},
|
||||
unmounted () {
|
||||
unmounted() {
|
||||
if (this.autohideFloatingPostButton) {
|
||||
this.deactivateFloatingPostButtonAutohide()
|
||||
}
|
||||
window.removeEventListener('resize', this.handleOSK)
|
||||
},
|
||||
computed: {
|
||||
isLoggedIn () {
|
||||
isLoggedIn() {
|
||||
return !!this.$store.state.users.currentUser
|
||||
},
|
||||
isHidden () {
|
||||
if (HIDDEN_FOR_PAGES.has(this.$route.name)) { return true }
|
||||
isHidden() {
|
||||
if (HIDDEN_FOR_PAGES.has(this.$route.name)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
|
||||
return (
|
||||
this.autohideFloatingPostButton && (this.hidden || this.inputActive)
|
||||
)
|
||||
},
|
||||
isPersistent () {
|
||||
isPersistent() {
|
||||
return !!this.$store.getters.mergedConfig.alwaysShowNewPostButton
|
||||
},
|
||||
autohideFloatingPostButton () {
|
||||
autohideFloatingPostButton() {
|
||||
return !!this.$store.getters.mergedConfig.autohideFloatingPostButton
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
autohideFloatingPostButton: function (isEnabled) {
|
||||
|
|
@ -60,21 +58,21 @@ const MobilePostStatusButton = {
|
|||
} else {
|
||||
this.deactivateFloatingPostButtonAutohide()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
activateFloatingPostButtonAutohide () {
|
||||
activateFloatingPostButtonAutohide() {
|
||||
window.addEventListener('scroll', this.handleScrollStart)
|
||||
window.addEventListener('scroll', this.handleScrollEnd)
|
||||
},
|
||||
deactivateFloatingPostButtonAutohide () {
|
||||
deactivateFloatingPostButtonAutohide() {
|
||||
window.removeEventListener('scroll', this.handleScrollStart)
|
||||
window.removeEventListener('scroll', this.handleScrollEnd)
|
||||
},
|
||||
openPostForm () {
|
||||
openPostForm() {
|
||||
usePostStatusStore().openPostStatusModal()
|
||||
},
|
||||
handleOSK () {
|
||||
handleOSK() {
|
||||
// This is a big hack: we're guessing from changed window sizes if the
|
||||
// on-screen keyboard is active or not. This is only really important
|
||||
// for phones in portrait mode and it's more important to show the button
|
||||
|
|
@ -94,20 +92,28 @@ const MobilePostStatusButton = {
|
|||
this.inputActive = false
|
||||
}
|
||||
},
|
||||
handleScrollStart: debounce(function () {
|
||||
if (window.scrollY > this.oldScrollPos) {
|
||||
this.hidden = true
|
||||
} else {
|
||||
this.hidden = false
|
||||
}
|
||||
this.oldScrollPos = window.scrollY
|
||||
}, 100, { leading: true, trailing: false }),
|
||||
handleScrollStart: debounce(
|
||||
function () {
|
||||
if (window.scrollY > this.oldScrollPos) {
|
||||
this.hidden = true
|
||||
} else {
|
||||
this.hidden = false
|
||||
}
|
||||
this.oldScrollPos = window.scrollY
|
||||
},
|
||||
100,
|
||||
{ leading: true, trailing: false },
|
||||
),
|
||||
|
||||
handleScrollEnd: debounce(function () {
|
||||
this.hidden = false
|
||||
this.oldScrollPos = window.scrollY
|
||||
}, 100, { leading: false, trailing: true })
|
||||
}
|
||||
handleScrollEnd: debounce(
|
||||
function () {
|
||||
this.hidden = false
|
||||
this.oldScrollPos = window.scrollY
|
||||
},
|
||||
100,
|
||||
{ leading: false, trailing: true },
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export default MobilePostStatusButton
|
||||
|
|
|
|||
|
|
@ -13,27 +13,27 @@
|
|||
<script>
|
||||
export default {
|
||||
provide: {
|
||||
popoversZLayer: 'modals'
|
||||
popoversZLayer: 'modals',
|
||||
},
|
||||
props: {
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
default: true,
|
||||
},
|
||||
noBackground: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['backdropClicked'],
|
||||
computed: {
|
||||
classes () {
|
||||
classes() {
|
||||
return {
|
||||
'modal-background': !this.noBackground,
|
||||
open: this.isOpen
|
||||
open: this.isOpen,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ export default {
|
|||
selector: ['.modal-view', '#modal', '.shout-panel'],
|
||||
lazy: true,
|
||||
notEditable: true,
|
||||
validInnerComponents: [
|
||||
'Panel'
|
||||
],
|
||||
defaultRules: []
|
||||
validInnerComponents: ['Panel'],
|
||||
defaultRules: [],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import DialogModal from '../dialog_modal/dialog_modal.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faChevronDown)
|
||||
|
||||
const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
|
||||
|
|
@ -15,10 +15,8 @@ const SANDBOX = 'mrf_tag:sandbox'
|
|||
const QUARANTINE = 'mrf_tag:quarantine'
|
||||
|
||||
const ModerationTools = {
|
||||
props: [
|
||||
'user'
|
||||
],
|
||||
data () {
|
||||
props: ['user'],
|
||||
data() {
|
||||
return {
|
||||
tags: {
|
||||
FORCE_NSFW,
|
||||
|
|
@ -27,92 +25,124 @@ const ModerationTools = {
|
|||
DISABLE_REMOTE_SUBSCRIPTION,
|
||||
DISABLE_ANY_SUBSCRIPTION,
|
||||
SANDBOX,
|
||||
QUARANTINE
|
||||
QUARANTINE,
|
||||
},
|
||||
showDeleteUserDialog: false,
|
||||
toggled: false
|
||||
toggled: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
DialogModal,
|
||||
Popover
|
||||
Popover,
|
||||
},
|
||||
computed: {
|
||||
tagsSet () {
|
||||
tagsSet() {
|
||||
return new Set(this.user.tags)
|
||||
},
|
||||
canGrantRole () {
|
||||
return this.user.is_local && !this.user.deactivated && this.$store.state.users.currentUser.role === 'admin'
|
||||
canGrantRole() {
|
||||
return (
|
||||
this.user.is_local &&
|
||||
!this.user.deactivated &&
|
||||
this.$store.state.users.currentUser.role === 'admin'
|
||||
)
|
||||
},
|
||||
canChangeActivationState () {
|
||||
canChangeActivationState() {
|
||||
return this.privileged('users_manage_activation_state')
|
||||
},
|
||||
canDeleteAccount () {
|
||||
canDeleteAccount() {
|
||||
return this.privileged('users_delete')
|
||||
},
|
||||
canUseTagPolicy () {
|
||||
return this.$store.state.instance.tagPolicyAvailable && this.privileged('users_manage_tags')
|
||||
}
|
||||
canUseTagPolicy() {
|
||||
return (
|
||||
this.$store.state.instance.tagPolicyAvailable &&
|
||||
this.privileged('users_manage_tags')
|
||||
)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hasTag (tagName) {
|
||||
hasTag(tagName) {
|
||||
return this.tagsSet.has(tagName)
|
||||
},
|
||||
privileged (privilege) {
|
||||
privileged(privilege) {
|
||||
return this.$store.state.users.currentUser.privileges.includes(privilege)
|
||||
},
|
||||
toggleTag (tag) {
|
||||
toggleTag(tag) {
|
||||
const store = this.$store
|
||||
if (this.tagsSet.has(tag)) {
|
||||
store.state.api.backendInteractor.untagUser({ user: this.user, tag }).then(response => {
|
||||
if (!response.ok) { return }
|
||||
store.commit('untagUser', { user: this.user, tag })
|
||||
})
|
||||
store.state.api.backendInteractor
|
||||
.untagUser({ user: this.user, tag })
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return
|
||||
}
|
||||
store.commit('untagUser', { user: this.user, tag })
|
||||
})
|
||||
} else {
|
||||
store.state.api.backendInteractor.tagUser({ user: this.user, tag }).then(response => {
|
||||
if (!response.ok) { return }
|
||||
store.commit('tagUser', { user: this.user, tag })
|
||||
})
|
||||
store.state.api.backendInteractor
|
||||
.tagUser({ user: this.user, tag })
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return
|
||||
}
|
||||
store.commit('tagUser', { user: this.user, tag })
|
||||
})
|
||||
}
|
||||
},
|
||||
toggleRight (right) {
|
||||
toggleRight(right) {
|
||||
const store = this.$store
|
||||
if (this.user.rights[right]) {
|
||||
store.state.api.backendInteractor.deleteRight({ user: this.user, right }).then(response => {
|
||||
if (!response.ok) { return }
|
||||
store.commit('updateRight', { user: this.user, right, value: false })
|
||||
})
|
||||
store.state.api.backendInteractor
|
||||
.deleteRight({ user: this.user, right })
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return
|
||||
}
|
||||
store.commit('updateRight', {
|
||||
user: this.user,
|
||||
right,
|
||||
value: false,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
store.state.api.backendInteractor.addRight({ user: this.user, right }).then(response => {
|
||||
if (!response.ok) { return }
|
||||
store.commit('updateRight', { user: this.user, right, value: true })
|
||||
})
|
||||
store.state.api.backendInteractor
|
||||
.addRight({ user: this.user, right })
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return
|
||||
}
|
||||
store.commit('updateRight', { user: this.user, right, value: true })
|
||||
})
|
||||
}
|
||||
},
|
||||
toggleActivationStatus () {
|
||||
toggleActivationStatus() {
|
||||
this.$store.dispatch('toggleActivationStatus', { user: this.user })
|
||||
},
|
||||
deleteUserDialog (show) {
|
||||
deleteUserDialog(show) {
|
||||
this.showDeleteUserDialog = show
|
||||
},
|
||||
deleteUser () {
|
||||
deleteUser() {
|
||||
const store = this.$store
|
||||
const user = this.user
|
||||
const { id, name } = user
|
||||
store.state.api.backendInteractor.deleteUser({ user })
|
||||
.then(() => {
|
||||
this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id)
|
||||
const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'
|
||||
const isTargetUser = this.$route.params.name === name || this.$route.params.id === id
|
||||
if (isProfile && isTargetUser) {
|
||||
window.history.back()
|
||||
}
|
||||
})
|
||||
store.state.api.backendInteractor.deleteUser({ user }).then(() => {
|
||||
this.$store.dispatch(
|
||||
'markStatusesAsDeleted',
|
||||
(status) => user.id === status.user.id,
|
||||
)
|
||||
const isProfile =
|
||||
this.$route.name === 'external-user-profile' ||
|
||||
this.$route.name === 'user-profile'
|
||||
const isTargetUser =
|
||||
this.$route.params.name === name || this.$route.params.id === id
|
||||
if (isProfile && isTargetUser) {
|
||||
window.history.back()
|
||||
}
|
||||
})
|
||||
},
|
||||
setToggled (value) {
|
||||
setToggled(value) {
|
||||
this.toggled = value
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default ModerationTools
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { mapState } from 'vuex'
|
||||
import { get } from 'lodash'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
/**
|
||||
* This is for backwards compatibility. We originally didn't recieve
|
||||
|
|
@ -8,7 +8,7 @@ import { get } from 'lodash'
|
|||
* to add an extra "info" key.
|
||||
*/
|
||||
const toInstanceReasonObject = (instances, info, key) => {
|
||||
return instances.map(instance => {
|
||||
return instances.map((instance) => {
|
||||
if (info[key] && info[key][instance] && info[key][instance].reason) {
|
||||
return { instance, reason: info[key][instance].reason }
|
||||
}
|
||||
|
|
@ -19,56 +19,82 @@ const toInstanceReasonObject = (instances, info, key) => {
|
|||
const MRFTransparencyPanel = {
|
||||
computed: {
|
||||
...mapState({
|
||||
federationPolicy: state => get(state, 'instance.federationPolicy'),
|
||||
mrfPolicies: state => get(state, 'instance.federationPolicy.mrf_policies', []),
|
||||
quarantineInstances: state => toInstanceReasonObject(
|
||||
get(state, 'instance.federationPolicy.quarantined_instances', []),
|
||||
get(state, 'instance.federationPolicy.quarantined_instances_info', []),
|
||||
'quarantined_instances'
|
||||
),
|
||||
acceptInstances: state => toInstanceReasonObject(
|
||||
get(state, 'instance.federationPolicy.mrf_simple.accept', []),
|
||||
get(state, 'instance.federationPolicy.mrf_simple_info', []),
|
||||
'accept'
|
||||
),
|
||||
rejectInstances: state => toInstanceReasonObject(
|
||||
get(state, 'instance.federationPolicy.mrf_simple.reject', []),
|
||||
get(state, 'instance.federationPolicy.mrf_simple_info', []),
|
||||
'reject'
|
||||
),
|
||||
ftlRemovalInstances: state => toInstanceReasonObject(
|
||||
get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []),
|
||||
get(state, 'instance.federationPolicy.mrf_simple_info', []),
|
||||
'federated_timeline_removal'
|
||||
),
|
||||
mediaNsfwInstances: state => toInstanceReasonObject(
|
||||
get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []),
|
||||
get(state, 'instance.federationPolicy.mrf_simple_info', []),
|
||||
'media_nsfw'
|
||||
),
|
||||
mediaRemovalInstances: state => toInstanceReasonObject(
|
||||
get(state, 'instance.federationPolicy.mrf_simple.media_removal', []),
|
||||
get(state, 'instance.federationPolicy.mrf_simple_info', []),
|
||||
'media_removal'
|
||||
),
|
||||
keywordsFtlRemoval: state => get(state, 'instance.federationPolicy.mrf_keyword.federated_timeline_removal', []),
|
||||
keywordsReject: state => get(state, 'instance.federationPolicy.mrf_keyword.reject', []),
|
||||
keywordsReplace: state => get(state, 'instance.federationPolicy.mrf_keyword.replace', [])
|
||||
federationPolicy: (state) => get(state, 'instance.federationPolicy'),
|
||||
mrfPolicies: (state) =>
|
||||
get(state, 'instance.federationPolicy.mrf_policies', []),
|
||||
quarantineInstances: (state) =>
|
||||
toInstanceReasonObject(
|
||||
get(state, 'instance.federationPolicy.quarantined_instances', []),
|
||||
get(
|
||||
state,
|
||||
'instance.federationPolicy.quarantined_instances_info',
|
||||
[],
|
||||
),
|
||||
'quarantined_instances',
|
||||
),
|
||||
acceptInstances: (state) =>
|
||||
toInstanceReasonObject(
|
||||
get(state, 'instance.federationPolicy.mrf_simple.accept', []),
|
||||
get(state, 'instance.federationPolicy.mrf_simple_info', []),
|
||||
'accept',
|
||||
),
|
||||
rejectInstances: (state) =>
|
||||
toInstanceReasonObject(
|
||||
get(state, 'instance.federationPolicy.mrf_simple.reject', []),
|
||||
get(state, 'instance.federationPolicy.mrf_simple_info', []),
|
||||
'reject',
|
||||
),
|
||||
ftlRemovalInstances: (state) =>
|
||||
toInstanceReasonObject(
|
||||
get(
|
||||
state,
|
||||
'instance.federationPolicy.mrf_simple.federated_timeline_removal',
|
||||
[],
|
||||
),
|
||||
get(state, 'instance.federationPolicy.mrf_simple_info', []),
|
||||
'federated_timeline_removal',
|
||||
),
|
||||
mediaNsfwInstances: (state) =>
|
||||
toInstanceReasonObject(
|
||||
get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []),
|
||||
get(state, 'instance.federationPolicy.mrf_simple_info', []),
|
||||
'media_nsfw',
|
||||
),
|
||||
mediaRemovalInstances: (state) =>
|
||||
toInstanceReasonObject(
|
||||
get(state, 'instance.federationPolicy.mrf_simple.media_removal', []),
|
||||
get(state, 'instance.federationPolicy.mrf_simple_info', []),
|
||||
'media_removal',
|
||||
),
|
||||
keywordsFtlRemoval: (state) =>
|
||||
get(
|
||||
state,
|
||||
'instance.federationPolicy.mrf_keyword.federated_timeline_removal',
|
||||
[],
|
||||
),
|
||||
keywordsReject: (state) =>
|
||||
get(state, 'instance.federationPolicy.mrf_keyword.reject', []),
|
||||
keywordsReplace: (state) =>
|
||||
get(state, 'instance.federationPolicy.mrf_keyword.replace', []),
|
||||
}),
|
||||
hasInstanceSpecificPolicies () {
|
||||
return this.quarantineInstances.length ||
|
||||
hasInstanceSpecificPolicies() {
|
||||
return (
|
||||
this.quarantineInstances.length ||
|
||||
this.acceptInstances.length ||
|
||||
this.rejectInstances.length ||
|
||||
this.ftlRemovalInstances.length ||
|
||||
this.mediaNsfwInstances.length ||
|
||||
this.mediaRemovalInstances.length
|
||||
)
|
||||
},
|
||||
hasKeywordPolicies () {
|
||||
return this.keywordsFtlRemoval.length ||
|
||||
hasKeywordPolicies() {
|
||||
return (
|
||||
this.keywordsFtlRemoval.length ||
|
||||
this.keywordsReject.length ||
|
||||
this.keywordsReplace.length
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default MRFTransparencyPanel
|
||||
|
|
|
|||
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