diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js index ea46ce6f68..e07bb7a255 100644 --- a/build/webpack.base.conf.js +++ b/build/webpack.base.conf.js @@ -95,7 +95,8 @@ module.exports = { }, plugins: [ new ServiceWorkerWebpackPlugin({ - entry: path.join(__dirname, '..', 'src/sw.js') + entry: path.join(__dirname, '..', 'src/sw.js'), + filename: 'sw-pleroma.js' }) ] } diff --git a/src/App.scss b/src/App.scss index 470860064d..d3721b324c 100644 --- a/src/App.scss +++ b/src/App.scss @@ -425,6 +425,12 @@ main-router { border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); + + .faint { + color: $fallback--faint; + color: var(--panelFaint, $fallback--faint); + } + a { color: $fallback--link; color: var(--panelLink, $fallback--link) diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 505005825d..5693dcc65a 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -89,6 +89,8 @@ const afterStoreSetup = ({ store, i18n }) => { if ((config.chatDisabled)) { store.dispatch('disableChat') + } else { + store.dispatch('initializeSocket') } const router = new VueRouter({ diff --git a/src/components/link-preview/link-preview.js b/src/components/link-preview/link-preview.js new file mode 100644 index 0000000000..2f6da55e9f --- /dev/null +++ b/src/components/link-preview/link-preview.js @@ -0,0 +1,21 @@ +const LinkPreview = { + name: 'LinkPreview', + props: [ + 'card', + 'size', + 'nsfw' + ], + computed: { + 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.nsfw && this.size !== 'hide' + }, + useDescription () { + return this.card.description && /\S/.test(this.card.description) + } + } +} + +export default LinkPreview diff --git a/src/components/link-preview/link-preview.vue b/src/components/link-preview/link-preview.vue new file mode 100644 index 0000000000..9b3f2550b8 --- /dev/null +++ b/src/components/link-preview/link-preview.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index ad7c53f9fb..3aa0a793f0 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -44,6 +44,7 @@ .nav-panel .panel { overflow: hidden; + box-shadow: var(--panelShadow); } .nav-panel ul { list-style: none; diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index ea32bbd0d3..5e95631a99 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -13,6 +13,11 @@ const Notifications = { notificationsFetcher.startFetching({ store, credentials }) }, + data () { + return { + bottomedOut: false + } + }, computed: { notifications () { return notificationsFromStore(this.$store) @@ -28,6 +33,9 @@ const Notifications = { }, unseenCount () { return this.unseenNotifications.length + }, + loading () { + return this.$store.state.statuses.notifications.loading } }, components: { @@ -49,10 +57,16 @@ const Notifications = { fetchOlderNotifications () { const store = this.$store const credentials = store.state.users.currentUser.credentials + store.commit('setNotificationsLoading', { value: true }) notificationsFetcher.fetchAndUpdate({ store, credentials, older: true + }).then(notifs => { + store.commit('setNotificationsLoading', { value: false }) + if (notifs.length === 0) { + this.bottomedOut = true + } }) } } diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue index 64f1872097..6f162b625f 100644 --- a/src/components/notifications/notifications.vue +++ b/src/components/notifications/notifications.vue @@ -18,10 +18,15 @@ diff --git a/src/components/status/status.js b/src/components/status/status.js index 9730eded98..ec4de516f2 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -6,10 +6,12 @@ import PostStatusForm from '../post_status_form/post_status_form.vue' import UserCardContent from '../user_card_content/user_card_content.vue' import StillImage from '../still-image/still-image.vue' import Gallery from '../gallery/gallery.vue' -import { filter, find } from 'lodash' -import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' +import LinkPreview from '../link-preview/link-preview.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 { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' +import { mentionMatchesUrl } from 'src/services/mention_matcher/mention_matcher.js' +import { filter, find } from 'lodash' const Status = { name: 'Status', @@ -33,7 +35,7 @@ const Status = { userExpanded: false, preview: null, showPreview: false, - showingTall: false, + showingTall: this.inConversation && this.focused, expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined' ? !this.$store.state.instance.collapseMessageWithSubject : !this.$store.state.config.collapseMessageWithSubject, @@ -81,7 +83,7 @@ const Status = { }, replyProfileLink () { if (this.isReply) { - return this.generateUserProfileLink(this.status.in_reply_to_status_id, this.replyToName) + return this.generateUserProfileLink(this.status.in_reply_to_user_id, this.replyToName) } }, retweet () { return !!this.statusoid.retweeted_status }, @@ -181,7 +183,7 @@ const Status = { return this.tallStatus }, showingMore () { - return this.showingTall || (this.status.summary && this.expandingSubject) + return (this.tallStatus && this.showingTall) || (this.status.summary && this.expandingSubject) }, nsfwClickthrough () { if (!this.status.nsfw) { @@ -243,7 +245,8 @@ const Status = { PostStatusForm, UserCardContent, StillImage, - Gallery + Gallery, + LinkPreview }, methods: { visibilityIcon (visibility) { @@ -258,11 +261,23 @@ const Status = { return 'icon-globe' } }, - linkClicked ({target}) { + linkClicked (event) { + let { target } = event if (target.tagName === 'SPAN') { target = target.parentNode } if (target.tagName === 'A') { + if (target.className.match(/mention/)) { + const href = target.getAttribute('href') + const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href)) + if (attn) { + event.stopPropagation() + event.preventDefault() + const link = this.generateUserProfileLink(attn.id, attn.screen_name) + this.$router.push(link) + return + } + } window.open(target.href, '_blank') } }, @@ -287,11 +302,11 @@ const Status = { toggleShowMore () { if (this.showingTall) { this.showingTall = false - } else if (this.expandingSubject) { + } else if (this.expandingSubject && this.status.summary) { this.expandingSubject = false } else if (this.hideTallStatus) { this.showingTall = true - } else if (this.hideSubjectStatus) { + } else if (this.hideSubjectStatus && this.status.summary) { this.expandingSubject = true } }, @@ -329,8 +344,13 @@ const Status = { if (this.status.id === id) { let rect = this.$el.getBoundingClientRect() if (rect.top < 100) { - window.scrollBy(0, rect.top - 200) + // Post is above screen, match its top to screen top + window.scrollBy(0, rect.top - 100) + } else if (rect.height >= (window.innerHeight - 50)) { + // Post we want to see is taller than screen so match its top to screen top + window.scrollBy(0, rect.top - 100) } else if (rect.bottom > window.innerHeight - 50) { + // Post is below screen, match its bottom to screen bottom window.scrollBy(0, rect.bottom - window.innerHeight + 50) } } diff --git a/src/components/status/status.vue b/src/components/status/status.vue index c1800d6466..c6e73e4e45 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -24,9 +24,9 @@
- + - +
@@ -112,6 +112,10 @@ />
+ +
@@ -235,6 +239,11 @@ vertical-align: bottom; flex-basis: 100%; + a { + display: inline-block; + word-break: break-all; + } + small { font-weight: lighter; } @@ -310,11 +319,6 @@ } } - a { - display: inline-block; - word-break: break-all; - } - .tall-status { position: relative; height: 220px; @@ -323,6 +327,8 @@ } .tall-status-hider { + display: inline-block; + word-break: break-all; position: absolute; height: 70px; margin-top: 150px; @@ -340,6 +346,8 @@ .status-unhider, .cw-status-hider { width: 100%; text-align: center; + display: inline-block; + word-break: break-all; } .status-content { diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 98da86607b..85e0a05528 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -16,7 +16,8 @@ const Timeline = { data () { return { paused: false, - unfocused: false + unfocused: false, + bottomedOut: false } }, computed: { @@ -95,7 +96,12 @@ const Timeline = { showImmediately: true, userId: this.userId, tag: this.tag - }).then(() => store.commit('setLoading', { timeline: this.timelineName, value: false })) + }).then(statuses => { + store.commit('setLoading', { timeline: this.timelineName, value: false }) + if (statuses.length === 0) { + this.bottomedOut = true + } + }) }, 1000, this), scrollLoad (e) { const bodyBRect = document.body.getBoundingClientRect() diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 6ba598c5d2..e3eea3bdb6 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -20,10 +20,15 @@
- + + - +
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 991062cd27..27e138b02a 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -36,7 +36,8 @@ const UserProfile = { return this.$route.params.name || this.user.screen_name }, isUs () { - return this.userId === this.$store.state.users.currentUser.id + return this.userId && this.$store.state.users.currentUser.id && + this.userId === this.$store.state.users.currentUser.id }, friends () { return this.user.friends diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js index dcce275abc..be799f5de5 100644 --- a/src/components/user_settings/user_settings.js +++ b/src/components/user_settings/user_settings.js @@ -10,7 +10,8 @@ const UserSettings = { newLocked: this.$store.state.users.currentUser.locked, newNoRichText: this.$store.state.users.currentUser.no_rich_text, newDefaultScope: this.$store.state.users.currentUser.default_scope, - newHideNetwork: this.$store.state.users.currentUser.hide_network, + hideFollowings: this.$store.state.users.currentUser.hide_followings, + hideFollowers: this.$store.state.users.currentUser.hide_followers, followList: null, followImportError: false, followsImported: false, @@ -66,7 +67,8 @@ const UserSettings = { /* eslint-disable camelcase */ const default_scope = this.newDefaultScope const no_rich_text = this.newNoRichText - const hide_network = this.newHideNetwork + const hide_followings = this.hideFollowings + const hide_followers = this.hideFollowers /* eslint-enable camelcase */ this.$store.state.api.backendInteractor .updateProfile({ @@ -78,7 +80,8 @@ const UserSettings = { /* eslint-disable camelcase */ default_scope, no_rich_text, - hide_network + hide_followings, + hide_followers /* eslint-enable camelcase */ }}).then((user) => { if (!user.error) { diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index 4bc2eeecca..253bbd34f2 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -30,8 +30,12 @@

- - + + +

+

+ +

diff --git a/src/i18n/de.json b/src/i18n/de.json index c87371e6e9..82860e9ebc 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -155,7 +155,8 @@ "notification_visibility_mentions": "Erwähnungen", "notification_visibility_repeats": "Wiederholungen", "no_rich_text_description": "Rich-Text Formatierungen von allen Beiträgen entfernen", - "hide_network_description": "Zeige nicht, wem ich folge und wer mir folgt", + "hide_followings_description": "Zeige nicht, wem ich folge", + "hide_followers_description": "Zeige nicht, wer mir folgt", "nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind", "panelRadius": "Panel", "pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist", diff --git a/src/i18n/en.json b/src/i18n/en.json index a719cb0525..b1356aaa11 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -49,7 +49,8 @@ "load_older": "Load older notifications", "notifications": "Notifications", "read": "Read!", - "repeated_you": "repeated your status" + "repeated_you": "repeated your status", + "no_more_notifications": "No more notifications" }, "post_status": { "new_status": "Post new status", @@ -160,7 +161,8 @@ "notification_visibility_mentions": "Mentions", "notification_visibility_repeats": "Repeats", "no_rich_text_description": "Strip rich text formatting from all posts", - "hide_network_description": "Don't show who I'm following and who's following me", + "hide_followings_description": "Don't show who I'm following", + "hide_followers_description": "Don't show who's following me", "nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding", "panelRadius": "Panels", "pause_on_unfocused": "Pause streaming when tab is not focused", @@ -320,7 +322,8 @@ "no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated", "repeated": "repeated", "show_new": "Show new", - "up_to_date": "Up-to-date" + "up_to_date": "Up-to-date", + "no_more_statuses": "No more statuses" }, "user_card": { "approve": "Approve", diff --git a/src/i18n/fi.json b/src/i18n/fi.json index 969d5a9e1b..122016b025 100644 --- a/src/i18n/fi.json +++ b/src/i18n/fi.json @@ -49,7 +49,8 @@ "load_older": "Lataa vanhempia ilmoituksia", "notifications": "Ilmoitukset", "read": "Lue!", - "repeated_you": "toisti viestisi" + "repeated_you": "toisti viestisi", + "no_more_notifications": "Ei enempää ilmoituksia" }, "post_status": { "new_status": "Uusi viesti", @@ -209,7 +210,8 @@ "no_retweet_hint": "Viesti ei ole julkinen, eikä sitä voi toistaa", "repeated": "toisti", "show_new": "Näytä uudet", - "up_to_date": "Ajantasalla" + "up_to_date": "Ajantasalla", + "no_more_statuses": "Ei enempää viestejä" }, "user_card": { "approve": "Hyväksy", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index 161856f036..37e96f6cae 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -157,7 +157,8 @@ "notification_visibility_mentions": "メンション", "notification_visibility_repeats": "リピート", "no_rich_text_description": "リッチテキストをつかわない", - "hide_network_description": "わたしがフォローしているひとと、わたしをフォローしているひとを、みせない", + "hide_followings_description": "フォローしている人を表示しない", + "hide_followers_description": "フォローしている人を表示しない", "nsfw_clickthrough": "NSFWなファイルをかくす", "panelRadius": "パネル", "pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index 4b69df0734..9f40be51a2 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -156,7 +156,8 @@ "notification_visibility_mentions": "멘션", "notification_visibility_repeats": "반복", "no_rich_text_description": "모든 게시물의 서식을 지우기", - "hide_network_description": "내 팔로우와 팔로워를 숨기기", + "hide_followings_description": "내가 팔로우하는 사람을 표시하지 않음", + "hide_followers_description": "나를 따르는 사람을 보여주지 마라.", "nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화", "panelRadius": "패널", "pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 0887bb5908..bf1e319fa4 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -127,7 +127,8 @@ "notification_visibility_mentions": "Упоминания", "notification_visibility_repeats": "Повторы", "no_rich_text_description": "Убрать форматирование из всех постов", - "hide_network_description": "Не показывать кого я читаю и кто меня читает", + "hide_followings_description": "Не показывать кого я читаю", + "hide_followers_description": "Не показывать кто читает меня", "nsfw_clickthrough": "Включить скрытие NSFW вложений", "panelRadius": "Панели", "pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе", diff --git a/src/modules/api.js b/src/modules/api.js index a61340c223..7bda13e777 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -20,6 +20,9 @@ const api = { removeFetcher (state, {timeline}) { delete state.fetchers[timeline] }, + setWsToken (state, token) { + state.wsToken = token + }, setSocket (state, socket) { state.socket = socket }, @@ -51,10 +54,14 @@ const api = { window.clearInterval(fetcher) store.commit('removeFetcher', {timeline}) }, - initializeSocket (store, token) { + setWsToken (store, token) { + store.commit('setWsToken', token) + }, + initializeSocket (store) { // Set up websocket connection if (!store.state.chatDisabled) { - let socket = new Socket('/socket', {params: {token: token}}) + const token = store.state.wsToken + const socket = new Socket('/socket', {params: {token}}) socket.connect() store.dispatch('initializeChat', socket) } diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 3d6ea2f7d4..566194550e 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -2,7 +2,7 @@ import { remove, slice, each, find, maxBy, minBy, merge, last, isArray } from 'l import apiService from '../services/api/api.service.js' // import parse from '../services/status_parser/status_parser.js' -const emptyTl = () => ({ +const emptyTl = (userId = 0) => ({ statuses: [], statusesObject: {}, faves: [], @@ -14,7 +14,7 @@ const emptyTl = () => ({ loading: false, followers: [], friends: [], - userId: 0, + userId, flushMarker: 0 }) @@ -28,6 +28,7 @@ export const defaultState = { minId: Number.POSITIVE_INFINITY, data: [], idStore: {}, + loading: false, error: false }, favorites: new Set(), @@ -319,7 +320,7 @@ export const mutations = { each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status }) }, clearTimeline (state, { timeline }) { - state.timelines[timeline] = emptyTl() + state.timelines[timeline] = emptyTl(state.timelines[timeline].userId) }, setFavorited (state, { status, value }) { const newStatus = state.allStatusesObject[status.id] @@ -348,6 +349,9 @@ export const mutations = { setError (state, { value }) { state.error = value }, + setNotificationsLoading (state, { value }) { + state.notifications.loading = value + }, setNotificationsError (state, { value }) { state.notifications.error = value }, @@ -376,6 +380,9 @@ const statuses = { setError ({ rootState, commit }, { value }) { commit('setError', { value }) }, + setNotificationsLoading ({ rootState, commit }, { value }) { + commit('setNotificationsLoading', { value }) + }, setNotificationsError ({ rootState, commit }, { value }) { commit('setNotificationsError', { value }) }, diff --git a/src/modules/users.js b/src/modules/users.js index d83f0dd898..d6ab47ea44 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -91,7 +91,9 @@ export const getters = { userById: state => id => state.users.find(user => user.id === id), userByName: state => name => - state.users.find(user => user.screen_name === name) + state.users.find(user => user.screen_name && + (user.screen_name.toLowerCase() === name.toLowerCase()) + ) } export const defaultState = { @@ -222,10 +224,10 @@ const users = { commit('setBackendInteractor', backendInteractorService(accessToken)) if (user.token) { - store.dispatch('initializeSocket', user.token) + store.dispatch('setWsToken', user.token) } - // Start getting fresh tweets. + // Start getting fresh posts. store.dispatch('startFetching', 'friends') // Get user mutes and follower info diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 5b0d865025..31b48cb6fe 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -130,7 +130,7 @@ const updateBanner = ({credentials, params}) => { // description const updateProfile = ({credentials, params}) => { // Always include these fields, because they might be empty or false - const fields = ['description', 'locked', 'no_rich_text', 'hide_network'] + const fields = ['description', 'locked', 'no_rich_text', 'hide_followings', 'hide_followers'] let url = PROFILE_UPDATE_URL const form = new FormData() diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index e69547b6f8..fa955ad6cd 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -100,7 +100,8 @@ export const parseUser = (data) => { output.rights = data.rights output.no_rich_text = data.no_rich_text output.default_scope = data.default_scope - output.hide_network = data.hide_network + output.hide_followings = data.hide_followings + output.hide_followers = data.hide_followers output.background_image = data.background_image // on mastoapi this info is contained in a "relationship" output.following = data.following @@ -215,6 +216,7 @@ export const parseStatus = (data) => { output.id = String(data.id) output.visibility = data.visibility + output.card = data.card output.created_at = new Date(data.created_at) // Converting to string, the right way. diff --git a/src/services/mention_matcher/mention_matcher.js b/src/services/mention_matcher/mention_matcher.js new file mode 100644 index 0000000000..2c1ed97064 --- /dev/null +++ b/src/services/mention_matcher/mention_matcher.js @@ -0,0 +1,9 @@ + +export const mentionMatchesUrl = (attention, url) => { + if (url === attention.statusnet_profile_url) { + return true + } + const [namepart, instancepart] = attention.screen_name.split('@') + const matchstring = new RegExp('://' + instancepart + '/.*' + namepart + '$', 'g') + return !!url.match(matchstring) +} diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index 1480cded45..b69ec643c7 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -24,6 +24,7 @@ const fetchAndUpdate = ({store, credentials, older = false}) => { return apiService.fetchTimeline(args) .then((notifications) => { update({store, notifications, older}) + return notifications }, () => store.dispatch('setNotificationsError', { value: true })) .catch(() => store.dispatch('setNotificationsError', { value: true })) } diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 727f6c6043..64f8f46848 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -29,12 +29,15 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false args['userId'] = userId args['tag'] = tag + const numStatusesBeforeFetch = timelineData.statuses.length + return apiService.fetchTimeline(args) .then((statuses) => { - if (!older && statuses.length >= 20 && !timelineData.loading && timelineData.statuses.length) { + if (!older && statuses.length >= 20 && !timelineData.loading && numStatusesBeforeFetch > 0) { store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId }) } update({store, statuses, timeline, showImmediately, userId}) + return statuses }, () => store.dispatch('setError', { value: true })) } diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js index 33628b9b28..01d2ce061f 100644 --- a/test/unit/specs/modules/statuses.spec.js +++ b/test/unit/specs/modules/statuses.spec.js @@ -240,6 +240,15 @@ describe('The Statuses module', () => { expect(state.timelines.public.visibleStatuses[0].favorited).to.eql(true) }) + it('keeps userId when clearing user timeline', () => { + const state = cloneDeep(defaultState) + state.timelines.user.userId = 123 + + mutations.clearTimeline(state, { timeline: 'user' }) + + expect(state.timelines.user.userId).to.eql(123) + }) + describe('notifications', () => { it('removes a notification when the notice gets removed', () => { const user = { id: '1' } diff --git a/test/unit/specs/modules/users.spec.js b/test/unit/specs/modules/users.spec.js index b0f3c51eeb..4d49ee2424 100644 --- a/test/unit/specs/modules/users.spec.js +++ b/test/unit/specs/modules/users.spec.js @@ -45,6 +45,17 @@ describe('The users module', () => { const expected = { screen_name: 'Guy', id: '1' } expect(getters.userByName(state)(name)).to.eql(expected) }) + + it('returns user with matching screen_name with different case', () => { + const state = { + users: [ + { screen_name: 'guy', id: '1' } + ] + } + const name = 'Guy' + const expected = { screen_name: 'guy', id: '1' } + expect(getters.userByName(state)(name)).to.eql(expected) + }) }) describe('getUserById', () => { diff --git a/test/unit/specs/services/mention_matcher/mention_matcher.spec.js b/test/unit/specs/services/mention_matcher/mention_matcher.spec.js new file mode 100644 index 0000000000..4f6f58fff3 --- /dev/null +++ b/test/unit/specs/services/mention_matcher/mention_matcher.spec.js @@ -0,0 +1,63 @@ +import * as MentionMatcher from 'src/services/mention_matcher/mention_matcher.js' + +const localAttn = () => ({ + id: 123, + is_local: true, + name: 'Guy', + screen_name: 'person', + statusnet_profile_url: 'https://instance.com/users/person' +}) + +const externalAttn = () => ({ + id: 123, + is_local: false, + name: 'Guy', + screen_name: 'person@instance.com', + statusnet_profile_url: 'https://instance.com/users/person' +}) + +describe('MentionMatcher', () => { + describe.only('mentionMatchesUrl', () => { + it('should match local mention', () => { + const attention = localAttn() + const url = 'https://instance.com/users/person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true) + }) + + it('should not match a local mention with same name but different instance', () => { + const attention = localAttn() + const url = 'https://website.com/users/person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false) + }) + + it('should match external pleroma mention', () => { + const attention = externalAttn() + const url = 'https://instance.com/users/person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true) + }) + + it('should not match external pleroma mention with same name but different instance', () => { + const attention = externalAttn() + const url = 'https://website.com/users/person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false) + }) + + it('should match external mastodon mention', () => { + const attention = externalAttn() + const url = 'https://instance.com/@person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true) + }) + + it('should not match external mastodon mention with same name but different instance', () => { + const attention = externalAttn() + const url = 'https://website.com/@person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false) + }) + }) +})