diff --git a/src/components/avatar_list/avatar_list.vue b/src/components/avatar_list/avatar_list.vue
index e1b6e971..a1ba2042 100644
--- a/src/components/avatar_list/avatar_list.vue
+++ b/src/components/avatar_list/avatar_list.vue
@@ -8,6 +8,7 @@
>
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
index 29e13575..b73fb095 100644
--- a/src/components/popover/popover.js
+++ b/src/components/popover/popover.js
@@ -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.
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 729f9010..5e8b6c8a 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -74,7 +74,10 @@
:user="statusoid.user"
/>
-
+
{{ retweeter }}
+
{{ status.user.name }}
@@ -137,11 +143,17 @@
>
{{ status.user.screen_name }}
+
@@ -225,8 +237,9 @@
:user-id="status.in_reply_to_user_id"
>
{{ replyToName }}
@@ -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;
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index df095de3..2be52a17 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -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
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index bf8d376e..a67b17c5 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -28,63 +28,68 @@
{{ $t("status.show_full_subject") }}
-
+
+ {{ $t("status.show_content") }}
+
+
+
+
+
+
+
+ {{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
+
+
+
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 533d61c2..9a483210 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -63,6 +63,7 @@
@{{ user.screen_name }}
diff --git a/src/components/user_popover/user_popover.js b/src/components/user_popover/user_popover.js
index b9143b89..2771342a 100644
--- a/src/components/user_popover/user_popover.js
+++ b/src/components/user_popover/user_popover.js
@@ -2,7 +2,8 @@
const UserPopover = {
name: 'UserPopover',
props: [
- 'userId'
+ 'userId',
+ 'focusedElement'
],
data () {
return {
diff --git a/src/components/user_popover/user_popover.vue b/src/components/user_popover/user_popover.vue
index 784481e7..80379988 100644
--- a/src/components/user_popover/user_popover.vue
+++ b/src/components/user_popover/user_popover.vue
@@ -1,10 +1,12 @@
@@ -34,6 +36,12 @@
+
+
+
diff --git a/src/modules/api.js b/src/modules/api.js
index 68402602..5e213f0d 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -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)
}
}
)
diff --git a/src/modules/chats.js b/src/modules/chats.js
index 228d6256..c7609018 100644
--- a/src/modules/chats.js
+++ b/src/modules/chats.js
@@ -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)
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 64f5b587..e108b2a7 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -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 })
diff --git a/src/services/chat_utils/chat_utils.js b/src/services/chat_utils/chat_utils.js
new file mode 100644
index 00000000..ab898ced
--- /dev/null
+++ b/src/services/chat_utils/chat_utils.js
@@ -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)
+}
diff --git a/src/services/desktop_notification_utils/desktop_notification_utils.js b/src/services/desktop_notification_utils/desktop_notification_utils.js
new file mode 100644
index 00000000..b84a1f75
--- /dev/null
+++ b/src/services/desktop_notification_utils/desktop_notification_utils.js
@@ -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)
+}
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 7ea8a16c..c1bf8535 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -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) {
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index 5cc19215..d912d19f 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -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)
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index d282074a..80be02ca 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -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 })
}
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index 214294eb..d0cddf84 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -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