fix minor bugs, preliminary support for status mention previews

This commit is contained in:
Shpuld Shpuldson 2020-07-21 16:30:25 +03:00
commit cebf4989e7
17 changed files with 213 additions and 117 deletions

View File

@ -8,6 +8,7 @@
>
<UserAvatar
:user="user"
no-popover="true"
class="avatar-small"
/>
</router-link>

View File

@ -18,6 +18,8 @@ const Popover = {
// Takes a x/y object and tells how many pixels to offset from
// anchor point on either axis
offset: Object,
// Takes an element to use for positioning over this.$el
offsetElement: null,
// Additional styles you may want for the popover container
popoverClass: String,
// Time in milliseconds until the popup appears, default is 100ms
@ -47,7 +49,9 @@ const Popover = {
// Popover will be anchored around this element, trigger ref is the container, so
// its children are what are inside the slot. Expect only one slot="trigger".
const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
const screenBox = anchorEl.getBoundingClientRect()
const positionElement = this.offsetElement ? this.offsetElement : anchorEl
const screenBox = positionElement.getBoundingClientRect()
console.log(positionElement, screenBox)
// Screen position of the origin point for popover
const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }
const content = this.$refs.content
@ -99,11 +103,11 @@ const Popover = {
const yOffset = (this.offset && this.offset.y) || 0
const translateY = usingTop
? -anchorEl.offsetHeight - yOffset - content.offsetHeight
? -positionElement.offsetHeight - yOffset - content.offsetHeight
: yOffset
const xOffset = (this.offset && this.offset.x) || 0
const translateX = (anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset + xOffset
const translateX = (positionElement.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset + xOffset
// Note, separate translateX and translateY avoids blurry text on chromium,
// single translate or translate3d resulted in blurry text.

View File

@ -74,7 +74,10 @@
:user="statusoid.user"
/>
<div class="media-body faint">
<span class="user-name">
<span
class="user-name"
:title="retweeter"
>
<UserPopover :user-id="statusoid.user.id">
<router-link
v-if="retweeterHtml"
@ -86,6 +89,7 @@
:to="retweeterProfileLink"
>{{ retweeter }}</router-link>
</UserPopover>
</span>
<i
class="fa icon-retweet retweeted"
@ -123,11 +127,13 @@
<h4
v-if="status.user.name_html"
class="user-name"
:title="status.user.name"
v-html="status.user.name_html"
/>
<h4
v-else
class="user-name"
:title="status.user.name"
>
{{ status.user.name }}
</h4>
@ -137,11 +143,17 @@
>
<router-link
class="account-name"
:title="status.user.screen_name"
:to="userProfileLink"
>
{{ status.user.screen_name }}
</router-link>
</UserPopover>
<img
v-if="!!(status.user && status.user.favicon)"
class="status-favicon"
:src="status.user.favicon"
>
</div>
<span class="heading-right">
@ -225,8 +237,9 @@
:user-id="status.in_reply_to_user_id"
>
<router-link
:to="replyProfileLink"
class="reply-to-accountname"
:title="replyToName"
:to="replyProfileLink"
>
{{ replyToName }}
</router-link>
@ -434,6 +447,12 @@ $status-margin: 0.75em;
}
}
.status-favicon {
height: 18px;
width: 18px;
margin-right: 0.4em;
}
.media-heading {
padding: 0;
vertical-align: bottom;

View File

@ -2,6 +2,7 @@ import Attachment from '../attachment/attachment.vue'
import Poll from '../poll/poll.vue'
import Gallery from '../gallery/gallery.vue'
import LinkPreview from '../link-preview/link-preview.vue'
import UserPopover from '../user_popover/user_popover.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import fileType from 'src/services/file_type/file_type.service'
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
@ -10,6 +11,13 @@ import { mapGetters, mapState } from 'vuex'
const StatusContent = {
name: 'StatusContent',
components: {
Attachment,
Poll,
Gallery,
LinkPreview,
UserPopover
},
props: [
'status',
'focused',
@ -22,7 +30,9 @@ const StatusContent = {
showingTall: this.fullContent || (this.inConversation && this.focused),
showingLongSubject: false,
// not as computed because it sets the initial state which will be changed later
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
focusedUserId: null,
focusedUserElement: null
}
},
computed: {
@ -142,12 +152,6 @@ const StatusContent = {
currentUser: state => state.users.currentUser
})
},
components: {
Attachment,
Poll,
Gallery,
LinkPreview
},
methods: {
linkClicked (event) {
const target = event.target.closest('.status-content a')
@ -175,6 +179,22 @@ const StatusContent = {
window.open(target.href, '_blank')
}
},
linkHover (event) {
const target = event.target.closest('.status-content a')
this.focusedUserId = null
if (target) {
if (target.className.match(/mention/)) {
const href = target.href
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
if (attn) {
event.stopPropagation()
event.preventDefault()
this.focusedUserId = attn.id
this.focusedUserElement = target
}
}
}
},
toggleShowMore () {
if (this.mightHideBecauseTall) {
this.showingTall = !this.showingTall

View File

@ -28,63 +28,68 @@
{{ $t("status.show_full_subject") }}
</a>
</div>
<div
:class="{'tall-status': hideTallStatus}"
class="status-content-wrapper"
<UserPopover
:user-id="focusedUserId"
>
<a
v-if="hideTallStatus"
class="tall-status-hider"
:class="{ 'tall-status-hider_focused': focused }"
href="#"
@click.prevent="toggleShowMore"
>
{{ $t("general.show_more") }}
</a>
<div
v-if="!hideSubjectStatus"
:class="{ 'single-line': singleLine }"
class="status-content media-body"
@click.prevent="linkClicked"
v-html="postBodyHtml"
/>
<a
v-if="hideSubjectStatus"
href="#"
class="cw-status-hider"
@click.prevent="toggleShowMore"
:class="{'tall-status': hideTallStatus}"
class="status-content-wrapper"
>
{{ $t("status.show_content") }}
<span
v-if="attachmentTypes.includes('image')"
class="icon-picture"
<a
v-if="hideTallStatus"
class="tall-status-hider"
:class="{ 'tall-status-hider_focused': focused }"
href="#"
@click.prevent="toggleShowMore"
>
{{ $t("general.show_more") }}
</a>
<div
v-if="!hideSubjectStatus"
:class="{ 'single-line': singleLine }"
class="status-content media-body"
@click.prevent="linkClicked"
@mouseover="linkHover"
v-html="postBodyHtml"
/>
<span
v-if="attachmentTypes.includes('video')"
class="icon-video"
/>
<span
v-if="attachmentTypes.includes('audio')"
class="icon-music"
/>
<span
v-if="attachmentTypes.includes('unknown')"
class="icon-doc"
/>
<span
v-if="status.card"
class="icon-link"
/>
</a>
<a
v-if="showingMore && !fullContent"
href="#"
class="status-unhider"
@click.prevent="toggleShowMore"
>
{{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
</a>
</div>
<a
v-if="hideSubjectStatus"
href="#"
class="cw-status-hider"
@click.prevent="toggleShowMore"
>
{{ $t("status.show_content") }}
<span
v-if="attachmentTypes.includes('image')"
class="icon-picture"
/>
<span
v-if="attachmentTypes.includes('video')"
class="icon-video"
/>
<span
v-if="attachmentTypes.includes('audio')"
class="icon-music"
/>
<span
v-if="attachmentTypes.includes('unknown')"
class="icon-doc"
/>
<span
v-if="status.card"
class="icon-link"
/>
</a>
<a
v-if="showingMore && !fullContent"
href="#"
class="status-unhider"
@click.prevent="toggleShowMore"
>
{{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
</a>
</div>
</UserPopover>
<div v-if="status.poll && status.poll.options">
<poll :base-poll="status.poll" />

View File

@ -63,6 +63,7 @@
<div class="bottom-line">
<router-link
class="user-screen-name"
:title="user.screen_name"
:to="userProfileLink(user)"
>
@{{ user.screen_name }}

View File

@ -2,7 +2,8 @@
const UserPopover = {
name: 'UserPopover',
props: [
'userId'
'userId',
'focusedElement'
],
data () {
return {

View File

@ -1,10 +1,12 @@
<template>
<Popover
v-if="userId"
class="user-popover-container"
trigger="hover"
popover-class="user-popover"
:bound-to="{ x: 'container' }"
:delay="200"
:offset-element="focusedElement"
@show="enter"
>
<template slot="trigger">
@ -34,6 +36,12 @@
</div>
</div>
</Popover>
<div
v-else
class="user-popover-container"
>
<slot />
</div>
</template>
<script src="./user_popover.js" ></script>

View File

@ -1,5 +1,6 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { WSConnectionStatus } from '../services/api/api.service.js'
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
import { Socket } from 'phoenix'
const api = {
@ -77,6 +78,7 @@ const api = {
messages: [message.chatUpdate.lastMessage]
})
dispatch('updateChat', { chat: message.chatUpdate })
maybeShowChatNotification(store, message.chatUpdate)
}
}
)

View File

@ -2,6 +2,7 @@ import Vue from 'vue'
import { find, omitBy, orderBy, sumBy } from 'lodash'
import chatService from '../services/chat_service/chat_service.js'
import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js'
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
const emptyChatList = () => ({
data: [],
@ -59,8 +60,12 @@ const chats = {
return chats
})
},
addNewChats ({ rootState, commit, dispatch, rootGetters }, { chats }) {
commit('addNewChats', { dispatch, chats, rootGetters })
addNewChats (store, { chats }) {
const { commit, dispatch, rootGetters } = store
const newChatMessageSideEffects = (chat) => {
maybeShowChatNotification(store, chat)
}
commit('addNewChats', { dispatch, chats, rootGetters, newChatMessageSideEffects })
},
updateChat ({ commit }, { chat }) {
commit('updateChat', { chat })
@ -130,13 +135,17 @@ const chats = {
setCurrentChatId (state, { chatId }) {
state.currentChatId = chatId
},
addNewChats (state, { _dispatch, chats, _rootGetters }) {
addNewChats (state, { chats, newChatMessageSideEffects }) {
chats.forEach((updatedChat) => {
const chat = getChatById(state, updatedChat.id)
if (chat) {
const isNewMessage = (chat.lastMessage && chat.lastMessage.id) !== (updatedChat.lastMessage && updatedChat.lastMessage.id)
chat.lastMessage = updatedChat.lastMessage
chat.unread = updatedChat.unread
if (isNewMessage && chat.unread) {
newChatMessageSideEffects(updatedChat)
}
} else {
state.chatList.data.push(updatedChat)
Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)

View File

@ -13,9 +13,8 @@ import {
omitBy
} from 'lodash'
import { set } from 'vue'
import { isStatusNotification, prepareNotificationObject } from '../services/notification_utils/notification_utils.js'
import { isStatusNotification, maybeShowNotification } from '../services/notification_utils/notification_utils.js'
import apiService from '../services/api/api.service.js'
import { muteWordHits } from '../services/status_parser/status_parser.js'
const emptyTl = (userId = 0) => ({
statuses: [],
@ -77,17 +76,6 @@ export const prepareStatus = (status) => {
return status
}
const visibleNotificationTypes = (rootState) => {
return [
rootState.config.notificationVisibility.likes && 'like',
rootState.config.notificationVisibility.mentions && 'mention',
rootState.config.notificationVisibility.repeats && 'repeat',
rootState.config.notificationVisibility.follows && 'follow',
rootState.config.notificationVisibility.moves && 'move',
rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reactions'
].filter(_ => _)
}
const mergeOrAdd = (arr, obj, item) => {
const oldItem = obj[item.id]
@ -325,7 +313,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
}
}
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters, newNotificationSideEffects }) => {
each(notifications, (notification) => {
if (isStatusNotification(notification.type)) {
notification.action = addStatusToGlobalStorage(state, notification.action).item
@ -348,27 +336,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
state.notifications.data.push(notification)
state.notifications.idStore[notification.id] = notification
if ('Notification' in window && window.Notification.permission === 'granted') {
const notifObj = prepareNotificationObject(notification, rootGetters.i18n)
const reasonsToMuteNotif = (
notification.seen ||
state.notifications.desktopNotificationSilence ||
!visibleNotificationTypes.includes(notification.type) ||
(
notification.type === 'mention' && status && (
status.muted ||
muteWordHits(status, rootGetters.mergedConfig.muteWords).length === 0
)
)
)
if (!reasonsToMuteNotif) {
let desktopNotification = new window.Notification(notifObj.title, notifObj)
// Chrome is known for not closing notifications automatically
// according to MDN, anyway.
setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
}
}
newNotificationSideEffects(notification)
} else if (notification.seen) {
state.notifications.idStore[notification.id].seen = true
}
@ -609,8 +577,13 @@ const statuses = {
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) {
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination })
},
addNewNotifications ({ rootState, commit, dispatch, rootGetters }, { notifications, older }) {
commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older, rootGetters })
addNewNotifications (store, { notifications, older }) {
const { commit, dispatch, rootGetters } = store
const newNotificationSideEffects = (notification) => {
maybeShowNotification(store, notification)
}
commit('addNewNotifications', { dispatch, notifications, older, rootGetters, newNotificationSideEffects })
},
setError ({ rootState, commit }, { value }) {
commit('setError', { value })

View File

@ -0,0 +1,19 @@
import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js'
export const maybeShowChatNotification = (store, chat) => {
if (!chat.lastMessage) return
if (store.rootState.chats.currentChatId === chat.id && !document.hidden) return
const opts = {
tag: chat.lastMessage.id,
title: chat.account.name,
icon: chat.account.profile_image_url,
body: chat.lastMessage.content
}
if (chat.lastMessage.attachment && chat.lastMessage.attachment.type === 'image') {
opts.image = chat.lastMessage.attachment.preview_url
}
showDesktopNotification(store.rootState, opts)
}

View File

@ -0,0 +1,9 @@
export const showDesktopNotification = (rootState, desktopNotificationOpts) => {
if (!('Notification' in window && window.Notification.permission === 'granted')) return
if (rootState.statuses.notifications.desktopNotificationSilence) { return }
const desktopNotification = new window.Notification(desktopNotificationOpts.title, desktopNotificationOpts)
// Chrome is known for not closing notifications automatically
// according to MDN, anyway.
setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
}

View File

@ -79,6 +79,7 @@ export const parseUser = (data) => {
const relationship = data.pleroma.relationship
output.background_image = data.pleroma.background_image
output.favicon = data.pleroma.favicon
output.token = data.pleroma.chat_token
if (relationship) {

View File

@ -1,16 +1,22 @@
import { filter, sortBy, includes } from 'lodash'
import { muteWordHits } from '../status_parser/status_parser.js'
import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js'
export const notificationsFromStore = store => store.state.statuses.notifications.data
export const visibleTypes = store => ([
store.state.config.notificationVisibility.likes && 'like',
store.state.config.notificationVisibility.mentions && 'mention',
store.state.config.notificationVisibility.repeats && 'repeat',
store.state.config.notificationVisibility.follows && 'follow',
store.state.config.notificationVisibility.followRequest && 'follow_request',
store.state.config.notificationVisibility.moves && 'move',
store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
].filter(_ => _))
export const visibleTypes = store => {
const rootState = store.rootState || store.state
return ([
rootState.config.notificationVisibility.likes && 'like',
rootState.config.notificationVisibility.mentions && 'mention',
rootState.config.notificationVisibility.repeats && 'repeat',
rootState.config.notificationVisibility.follows && 'follow',
rootState.config.notificationVisibility.followRequest && 'follow_request',
rootState.config.notificationVisibility.moves && 'move',
rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
].filter(_ => _))
}
const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction']
@ -32,6 +38,22 @@ const sortById = (a, b) => {
}
}
const isMutedNotification = (store, notification) => {
if (!notification.status) return
return notification.status.muted || muteWordHits(notification.status, store.rootGetters.mergedConfig.muteWords).length > 0
}
export const maybeShowNotification = (store, notification) => {
const rootState = store.rootState || store.state
if (notification.seen) return
if (!visibleTypes(store).includes(notification.type)) return
if (notification.type === 'mention' && isMutedNotification(store, notification)) return
const notificationObject = prepareNotificationObject(notification, store.rootGetters.i18n)
showDesktopNotification(rootState, notificationObject)
}
export const filteredNotificationsFromStore = (store, types) => {
// map is just to clone the array since sort mutates it and it causes some issues
let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)

View File

@ -35,7 +35,7 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
const notifications = timelineData.data
const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id)
const numUnseenNotifs = notifications.length - readNotifsIds.length
if (numUnseenNotifs > 0) {
if (numUnseenNotifs > 0 && readNotifsIds.length > 0) {
args['since'] = Math.max(...readNotifsIds)
fetchNotifications({ store, args, older })
}

View File

@ -43,7 +43,9 @@ const fetchAndUpdate = ({
args['userId'] = userId
args['tag'] = tag
args['withMuted'] = !hideMutedPosts
if (loggedIn) args['replyVisibility'] = replyVisibility
if (loggedIn && ['friends', 'public', 'publicAndExternal'].includes(timeline)) {
args['replyVisibility'] = replyVisibility
}
const numStatusesBeforeFetch = timelineData.statuses.length