Page MenuHomePhorge

No OneTemporary

Size
25 KB
Referenced Files
None
Subscribers
None
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 8692c61903..3edf21de7a 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -1,176 +1,201 @@
import StatusContent from '../status_content/status_content.vue'
import { mapState } from 'vuex'
import Status from '../status/status.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.vue'
import Report from '../report/report.vue'
import UserLink from '../user_link/user_link.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import UserPopover from '../user_popover/user_popover.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faCheck,
faTimes,
faStar,
faRetweet,
faUserPlus,
faEyeSlash,
faUser,
faSuitcaseRolling,
faExpandAlt,
faCompressAlt
} from '@fortawesome/free-solid-svg-icons'
library.add(
faCheck,
faTimes,
faStar,
faRetweet,
faUserPlus,
faUser,
faEyeSlash,
faSuitcaseRolling,
faExpandAlt,
faCompressAlt
)
const Notification = {
data () {
return {
+ selecting: false,
statusExpanded: false,
unmuted: false,
showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false
}
},
props: ['notification'],
emits: ['interacted'],
components: {
StatusContent,
UserAvatar,
UserCard,
Timeago,
Status,
Report,
RichContent,
UserPopover,
UserLink,
ConfirmModal
},
+ mounted () {
+ document.addEventListener('selectionchange', this.onContentSelect)
+ },
+ unmounted () {
+ document.removeEventListener('selectionchange', this.onContentSelect)
+ },
methods: {
toggleStatusExpanded () {
if (!this.expandable) return
this.statusExpanded = !this.statusExpanded
},
+ onContentSelect () {
+ const { isCollapsed, anchorNode, offsetNode } = document.getSelection()
+ if (isCollapsed) {
+ this.selecting = false
+ return
+ }
+ const within = this.$refs.root.contains(anchorNode) || this.$refs.root.contains(offsetNode)
+ if (within) {
+ this.selecting = true
+ } else {
+ this.selecting = false
+ }
+ },
+ onContentClick (e) {
+ if (!this.selecting && !e.target.closest('a') && !e.target.closest('button')) {
+ this.toggleStatusExpanded()
+ }
+ },
generateUserProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
},
getUser (notification) {
return this.$store.state.users.usersObject[notification.from_profile.id]
},
interacted () {
this.$emit('interacted')
},
toggleMute () {
this.unmuted = !this.unmuted
},
showApproveConfirmDialog () {
this.showingApproveConfirmDialog = true
},
hideApproveConfirmDialog () {
this.showingApproveConfirmDialog = false
},
showDenyConfirmDialog () {
this.showingDenyConfirmDialog = true
},
hideDenyConfirmDialog () {
this.showingDenyConfirmDialog = false
},
approveUser () {
if (this.shouldConfirmApprove) {
this.showApproveConfirmDialog()
} else {
this.doApprove()
}
},
doApprove () {
this.$emit('interacted')
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user)
this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id })
this.$store.dispatch('updateNotification', {
id: this.notification.id,
updater: notification => {
notification.type = 'follow'
}
})
this.hideApproveConfirmDialog()
},
denyUser () {
if (this.shouldConfirmDeny) {
this.showDenyConfirmDialog()
} else {
this.doDeny()
}
},
doDeny () {
this.$emit('interacted')
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
.then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id })
this.$store.dispatch('removeFollowRequest', this.user)
})
this.hideDenyConfirmDialog()
}
},
computed: {
userClass () {
return highlightClass(this.notification.from_profile)
},
userStyle () {
const highlight = this.$store.getters.mergedConfig.highlight
const user = this.notification.from_profile
return highlightStyle(highlight[user.screen_name])
},
expandable () {
return (new Set(['like', 'pleroma:emoji_reaction', 'repeat'])).has(this.notification.type)
},
user () {
return this.$store.getters.findUser(this.notification.from_profile.id)
},
userProfileLink () {
return this.generateUserProfileLink(this.user)
},
targetUser () {
return this.$store.getters.findUser(this.notification.target.id)
},
targetUserProfileLink () {
return this.generateUserProfileLink(this.targetUser)
},
needMute () {
return this.$store.getters.relationship(this.user.id).muting
},
isStatusNotification () {
return isStatusNotification(this.notification.type)
},
mergedConfig () {
return this.$store.getters.mergedConfig
},
shouldConfirmApprove () {
return this.mergedConfig.modalOnApproveFollow
},
shouldConfirmDeny () {
return this.mergedConfig.modalOnDenyFollow
},
...mapState({
currentUser: state => state.users.currentUser
})
}
}
export default Notification
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 76ede11e2d..b639b9c881 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -1,290 +1,292 @@
<template>
<article
v-if="notification.type === 'mention' || notification.type === 'status'"
+ ref="root"
>
<Status
class="Notification"
:compact="true"
:statusoid="notification.status"
@interacted="interacted"
/>
</article>
<article
class="NotificationParent"
:class="{ '-expandable': expandable }"
+ ref="root"
v-else
>
<div
v-if="needMute && !unmuted"
:id="'notif-' +notification.id"
:aria-expanded="statusExpanded"
:aria-controls="'notif-' +notification.id"
class="Notification container -muted"
>
<small>
<user-link
:user="notification.from_profile"
:at="false"
/>
</small>
<button
class="button-unstyled unmute"
@click.prevent="toggleMute"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="eye-slash"
/>
</button>
</div>
<div
v-else
class="Notification non-mention"
:class="[userClass, { highlighted: userStyle }, '-type--' + notification.type]"
:style="[ userStyle ]"
>
<a
class="avatar-container"
:href="$router.resolve(userProfileLink).href"
@click.prevent
>
<UserPopover
:user-id="notification.from_profile.id"
:overlay-centers="true"
>
<UserAvatar
class="post-avatar"
:compact="true"
:user="notification.from_profile"
/>
</UserPopover>
</a>
<div class="notification-right">
<span class="notification-details">
<div class="name-and-action">
<!-- eslint-disable vue/no-v-html -->
<bdi v-if="!!notification.from_profile.name_html">
<RichContent
class="username"
:title="'@'+notification.from_profile.screen_name_ui"
:html="notification.from_profile.name_html"
:emoji="notification.from_profile.emoji"
/>
</bdi>
<!-- eslint-enable vue/no-v-html -->
<span
v-else
class="username"
:title="'@'+notification.from_profile.screen_name_ui"
>
{{ notification.from_profile.name }}
</span>
{{ ' ' }}
<span v-if="notification.type === 'like'">
<FAIcon
class="type-icon"
icon="star"
/>
{{ ' ' }}
<small>{{ $t('notifications.favorited_you') }}</small>
</span>
<span v-if="notification.type === 'repeat'">
<FAIcon
class="type-icon"
icon="retweet"
:title="$t('tool_tip.repeat')"
/>
{{ ' ' }}
<small>{{ $t('notifications.repeated_you') }}</small>
</span>
<span v-if="notification.type === 'follow'">
<FAIcon
class="type-icon"
icon="user-plus"
/>
{{ ' ' }}
<small>{{ $t('notifications.followed_you') }}</small>
</span>
<span v-if="notification.type === 'follow_request'">
<FAIcon
class="type-icon"
icon="user"
/>
{{ ' ' }}
<small>{{ $t('notifications.follow_request') }}</small>
</span>
<span v-if="notification.type === 'move'">
<FAIcon
class="type-icon"
icon="suitcase-rolling"
/>
{{ ' ' }}
<small>{{ $t('notifications.migrated_to') }}</small>
</span>
<span v-if="notification.type === 'pleroma:emoji_reaction'">
<small>
<i18n-t
scope="global"
keypath="notifications.reacted_with"
>
<img
v-if="notification.emoji_url"
class="emoji-reaction-emoji emoji-reaction-emoji-image"
:src="notification.emoji_url"
:alt="notification.emoji"
:title="notification.emoji"
>
<span
v-else
class="emoji-reaction-emoji"
>{{ notification.emoji }}</span>
</i18n-t>
</small>
</span>
<span v-if="notification.type === 'pleroma:report'">
<small>{{ $t('notifications.submitted_report') }}</small>
</span>
<span v-if="notification.type === 'poll'">
<FAIcon
class="type-icon"
icon="poll-h"
/>
{{ ' ' }}
<small>{{ $t('notifications.poll_ended') }}</small>
</span>
</div>
<div
v-if="isStatusNotification"
class="timeago"
>
<router-link
v-if="notification.status"
:to="{ name: 'conversation', params: { id: notification.status.id } }"
class="timeago-link faint"
>
<Timeago
:time="notification.created_at"
:auto-update="240"
/>
</router-link>
<button
class="button-unstyled expand-icon"
:title="$t('tool_tip.toggle_expand')"
:aria-expanded="statusExpanded"
@click.prevent="toggleStatusExpanded"
>
<FAIcon
class="fa-scale-110"
fixed-width
:icon="statusExpanded ? 'compress-alt' : 'expand-alt'"
/>
</button>
</div>
<div
v-else
class="timeago"
>
<span class="faint">
<Timeago
:time="notification.created_at"
:auto-update="240"
/>
</span>
</div>
<button
v-if="needMute"
class="button-unstyled"
:title="$t('tool_tip.toggle_mute')"
:aria-expanded="!unmuted"
@click.prevent="toggleMute"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="eye-slash"
/>
</button>
</span>
<div
v-if="notification.type === 'follow' || notification.type === 'follow_request'"
class="follow-text"
>
<user-link
class="follow-name"
:user="notification.from_profile"
/>
<div
v-if="notification.type === 'follow_request'"
style="white-space: nowrap;"
>
<button
class="button-unstyled"
:title="$t('tool_tip.accept_follow_request')"
@click="approveUser()"
>
<FAIcon
icon="check"
class="fa-scale-110 fa-old-padding follow-request-accept"
/>
</button>
<button
class="button-unstyled"
:title="$t('tool_tip.reject_follow_request')"
@click="denyUser()"
>
<FAIcon
icon="times"
class="fa-scale-110 fa-old-padding follow-request-reject"
/>
</button>
</div>
</div>
<div
v-else-if="notification.type === 'move'"
class="move-text"
>
<user-link
:user="notification.target"
/>
</div>
<Report
v-else-if="notification.type === 'pleroma:report'"
:report-id="notification.report.id"
/>
<template v-else>
<StatusContent
class="status-content"
:compact="!statusExpanded"
:status="notification.status"
:collapse="!statusExpanded"
- @click="toggleStatusExpanded()"
+ @click="onContentClick"
/>
</template>
</div>
</div>
<teleport to="#modal">
<confirm-modal
v-if="showingApproveConfirmDialog"
:title="$t('user_card.approve_confirm_title')"
:confirm-text="$t('user_card.approve_confirm_accept_button')"
:cancel-text="$t('user_card.approve_confirm_cancel_button')"
@accepted="doApprove"
@cancelled="hideApproveConfirmDialog"
>
{{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
<confirm-modal
v-if="showingDenyConfirmDialog"
:title="$t('user_card.deny_confirm_title')"
:confirm-text="$t('user_card.deny_confirm_accept_button')"
:cancel-text="$t('user_card.deny_confirm_cancel_button')"
@accepted="doDeny"
@cancelled="hideDenyConfirmDialog"
>
{{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
</teleport>
</article>
</template>
<script src="./notification.js"></script>
<style src="./notification.scss" lang="scss"></style>
diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js
index e89d1d1738..f23055955a 100644
--- a/src/components/status_body/status_body.js
+++ b/src/components/status_body/status_body.js
@@ -1,152 +1,153 @@
import fileType from 'src/services/file_type/file_type.service'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faFile,
faMusic,
faImage,
faLink,
faPollH
} from '@fortawesome/free-solid-svg-icons'
library.add(
faFile,
faMusic,
faImage,
faLink,
faPollH
)
const StatusContent = {
name: 'StatusContent',
props: [
'compact',
'collapse', // replaces newlines with spaces
'status',
'focused',
'noHeading',
'fullContent',
'singleLine',
'showingTall',
'expandingSubject',
'showingLongSubject',
'toggleShowingTall',
'toggleExpandingSubject',
'toggleShowingLongSubject'
],
data () {
return {
postLength: this.status.text.length,
parseReadyDone: false
}
},
+ emits: ['parseReady'],
computed: {
localCollapseSubjectDefault () {
return this.mergedConfig.collapseMessageWithSubject
},
// This is a bit hacky, but we want to approximate post height before rendering
// so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
// as well as approximate line count by counting characters and approximating ~80
// per line.
//
// Using max-height + overflow: auto for status components resulted in false positives
// very often with japanese characters, and it was very annoying.
tallStatus () {
if (this.singleLine || this.compact) return false
const lengthScore = this.status.raw_html.split(/<p|<br/).length + this.postLength / 80
return lengthScore > 20
},
longSubject () {
return this.status.summary.length > 240
},
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
mightHideBecauseSubject () {
return !!this.status.summary && this.localCollapseSubjectDefault
},
mightHideBecauseTall () {
return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
},
hideSubjectStatus () {
return this.mightHideBecauseSubject && !this.expandingSubject
},
hideTallStatus () {
return this.mightHideBecauseTall && !this.showingTall
},
shouldShowToggle () {
return this.mightHideBecauseSubject || this.mightHideBecauseTall
},
toggleButtonClasses () {
return {
'cw-status-hider': !this.showingMore && this.mightHideBecauseSubject,
'tall-status-hider': !this.showingMore && this.mightHideBecauseTall,
'status-unhider': this.showingMore,
}
},
toggleText () {
if (this.showingMore) {
return this.mightHideBecauseSubject ? this.$t('status.hide_content') : this.$t('general.show_less')
} else {
return this.mightHideBecauseSubject ? this.$t('status.show_content') : this.$t('general.show_more')
}
},
showingMore () {
return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
},
attachmentTypes () {
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
},
collapsedStatus () {
return this.status.raw_html.replace(/(\n|<br\s?\/?>)/g, ' ')
},
...mapGetters(['mergedConfig'])
},
components: {
RichContent
},
mounted () {
this.status.attentions && this.status.attentions.forEach(attn => {
const { id } = attn
this.$store.dispatch('fetchUserIfMissing', id)
})
},
methods: {
onParseReady (event) {
if (this.parseReadyDone) return
this.parseReadyDone = true
this.$emit('parseReady', event)
const { writtenMentions, invisibleMentions } = event
writtenMentions
.filter(mention => !mention.notifying)
.forEach(mention => {
const { content, url } = mention
const cleanedString = content.replace(/<[^>]+?>/gi, '') // remove all tags
if (!cleanedString.startsWith('@')) return
const handle = cleanedString.slice(1)
const host = url.replace(/^https?:\/\//, '').replace(/\/.+?$/, '')
this.$store.dispatch('fetchUserIfMissing', `${handle}@${host}`)
})
/* This is a bit of a hack to make current tall status detector work
* with rich mentions. Invisible mentions are detected at RichContent level
* and also we generate plaintext version of mentions by stripping tags
* so here we subtract from post length by each mention that became invisible
* via MentionsLine
*/
this.postLength = invisibleMentions.reduce((acc, mention) => {
return acc - mention.textContent.length - 1
}, this.postLength)
},
toggleShowMore () {
if (this.mightHideBecauseTall) {
this.toggleShowingTall()
} else if (this.mightHideBecauseSubject) {
this.toggleExpandingSubject()
}
},
generateTagLink (tag) {
return `/tag/${tag}`
}
}
}
export default StatusContent
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index f61ba0d05d..364123f492 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -1,140 +1,141 @@
import Attachment from '../attachment/attachment.vue'
import Poll from '../poll/poll.vue'
import Gallery from '../gallery/gallery.vue'
import StatusBody from 'src/components/status_body/status_body.vue'
import LinkPreview from '../link-preview/link-preview.vue'
import { mapGetters, mapState } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faCircleNotch,
faFile,
faMusic,
faImage,
faLink,
faPollH
} from '@fortawesome/free-solid-svg-icons'
import { useMediaViewerStore } from 'src/stores/media_viewer'
library.add(
faCircleNotch,
faFile,
faMusic,
faImage,
faLink,
faPollH
)
const camelCase = name => name.charAt(0).toUpperCase() + name.slice(1)
const controlledOrUncontrolledGetters = list => list.reduce((res, name) => {
const camelized = camelCase(name)
const toggle = `controlledToggle${camelized}`
const controlledName = `controlled${camelized}`
const uncontrolledName = `uncontrolled${camelized}`
res[name] = function () {
return ((this.$data[toggle] !== undefined || this.$props[toggle] !== undefined) && this[toggle]) ? this[controlledName] : this[uncontrolledName]
}
return res
}, {})
const controlledOrUncontrolledToggle = (obj, name) => {
const camelized = camelCase(name)
const toggle = `controlledToggle${camelized}`
const uncontrolledName = `uncontrolled${camelized}`
if (obj[toggle]) {
obj[toggle]()
} else {
obj[uncontrolledName] = !obj[uncontrolledName]
}
}
const StatusContent = {
name: 'StatusContent',
props: [
'status',
'compact',
'collapse',
'focused',
'noHeading',
'fullContent',
'singleLine',
'controlledShowingTall',
'controlledExpandingSubject',
'controlledToggleShowingTall',
'controlledToggleExpandingSubject',
'controlledShowingLongSubject',
'controlledToggleShowingLongSubject'
],
+ emits: ['parseReady', 'mediaplay', 'mediapause'],
data () {
return {
uncontrolledShowingTall: this.fullContent || (this.inConversation && this.focused),
uncontrolledShowingLongSubject: false,
// not as computed because it sets the initial state which will be changed later
uncontrolledExpandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
}
},
computed: {
...controlledOrUncontrolledGetters(['showingTall', 'expandingSubject', 'showingLongSubject']),
statusCard () {
if (!this.status.card) return null
return this.status.card.url === this.status.quote_url ? null : this.status.card
},
hideAttachments () {
return (this.mergedConfig.hideAttachments && !this.inConversation) ||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation)
},
nsfwClickthrough () {
if (!this.status.nsfw) {
return false
}
if (this.status.summary && this.localCollapseSubjectDefault) {
return false
}
return true
},
localCollapseSubjectDefault () {
return this.mergedConfig.collapseMessageWithSubject
},
attachmentSize () {
if (this.compact) {
return 'small'
} else if ((this.mergedConfig.hideAttachments && !this.inConversation) ||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation) ||
(this.status.attachments.length > this.maxThumbnails)) {
return 'hide'
}
return 'normal'
},
maxThumbnails () {
return this.mergedConfig.maxThumbnails
},
...mapGetters(['mergedConfig']),
...mapState({
currentUser: state => state.users.currentUser
})
},
components: {
Attachment,
Poll,
Gallery,
LinkPreview,
StatusBody
},
methods: {
toggleShowingTall () {
controlledOrUncontrolledToggle(this, 'showingTall')
},
toggleExpandingSubject () {
controlledOrUncontrolledToggle(this, 'expandingSubject')
},
toggleShowingLongSubject () {
controlledOrUncontrolledToggle(this, 'showingLongSubject')
},
setMedia () {
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
return () => useMediaViewerStore().setMedia(attachments)
}
}
}
export default StatusContent

File Metadata

Mime Type
text/x-diff
Expires
Sat, Aug 29, 3:31 PM (1 d, 14 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1737147
Default Alt Text
(25 KB)

Event Timeline