+
-
-
{{ $t("general.show_more") }}
-
-
-
{{ $t("general.show_more") }}
{{ $t("general.show_less") }}
+ >
+ {{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
+
@@ -129,6 +128,12 @@ $status-margin: 0.75em;
flex: 1;
min-width: 0;
+ .status-content-wrapper {
+ display: flex;
+ flex-direction: column;
+ flex-wrap: nowrap;
+ }
+
.tall-status {
position: relative;
height: 220px;
@@ -136,7 +141,7 @@ $status-margin: 0.75em;
overflow-y: hidden;
z-index: 1;
.status-content {
- height: 100%;
+ min-height: 0;
mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
linear-gradient(to top, white, white);
/* Autoprefixed seem to ignore this one, and also syntax is different */
@@ -176,6 +181,38 @@ $status-margin: 0.75em;
}
}
+ .summary-wrapper {
+ margin-bottom: 0.5em;
+ border-style: solid;
+ border-width: 0 0 1px 0;
+ border-color: var(--border, $fallback--border);
+ flex-grow: 0;
+ }
+
+ .summary {
+ font-style: italic;
+ padding-bottom: 0.5em;
+ }
+
+ .tall-subject {
+ position: relative;
+ .summary {
+ max-height: 2em;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ }
+
+ .tall-subject-hider {
+ display: inline-block;
+ word-break: break-all;
+ // position: absolute;
+ width: 100%;
+ text-align: center;
+ padding-bottom: 0.5em;
+ }
+
.status-content {
font-family: var(--postFont, sans-serif);
line-height: 1.4em;
diff --git a/src/components/status_popover/status_popover.js b/src/components/status_popover/status_popover.js
index 159132a9e..51e7680c6 100644
--- a/src/components/status_popover/status_popover.js
+++ b/src/components/status_popover/status_popover.js
@@ -22,6 +22,10 @@ const StatusPopover = {
methods: {
enter () {
if (!this.status) {
+ if (!this.statusId) {
+ this.error = true
+ return
+ }
this.$store.dispatch('fetchStatus', this.statusId)
.then(data => (this.error = false))
.catch(e => (this.error = true))
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index d93fd8c31..7768fd416 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -46,11 +46,15 @@ const Timeline = {
newStatusCount () {
return this.timeline.newStatusCount
},
- newStatusCountStr () {
+ showLoadButton () {
+ if (this.timelineError || this.errorData) return false
+ return this.timeline.newStatusCount > 0 || this.timeline.flushMarker !== 0
+ },
+ loadButtonString () {
if (this.timeline.flushMarker !== 0) {
- return ''
+ return this.$t('timeline.reload')
} else {
- return ` (${this.newStatusCount})`
+ return `${this.$t('timeline.show_new')} (${this.newStatusCount})`
}
},
classes () {
@@ -123,8 +127,6 @@ const Timeline = {
if (e.key === '.') this.showNewStatuses()
},
showNewStatuses () {
- if (this.newStatusCount === 0) return
-
if (this.timeline.flushMarker !== 0) {
this.$store.commit('clearTimeline', { timeline: this.timelineName, excludeUserId: true })
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
@@ -146,7 +148,7 @@ const Timeline = {
showImmediately: true,
userId: this.userId,
tag: this.tag
- }).then(statuses => {
+ }).then(({ statuses }) => {
store.commit('setLoading', { timeline: this.timelineName, value: false })
if (statuses && statuses.length === 0) {
this.bottomedOut = true
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index ef3bb4c86..373eb638f 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -19,14 +19,14 @@
{{ errorData.statusText }}
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index c4a5ce9d6..9529d7f6c 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -70,10 +70,20 @@
>
@{{ user.screen_name }}
- {{ visibleRole }}
+
+
+ {{ visibleRole }}
+
+
+ bot
+
+
{
- const persistedState = await createPersistedState(persistedStateOptions)
+ let storageError = false
+ const plugins = [pushNotifications]
+ try {
+ const persistedState = await createPersistedState(persistedStateOptions)
+ plugins.push(persistedState)
+ } catch (e) {
+ console.error(e)
+ storageError = true
+ }
const store = new Vuex.Store({
modules: {
i18n: {
@@ -85,11 +93,13 @@ const persistedStateOptions = {
polls: pollsModule,
postStatus: postStatusModule
},
- plugins: [persistedState, pushNotifications],
+ plugins,
strict: false // Socket modifies itself, let's ignore this for now.
// strict: process.env.NODE_ENV !== 'production'
})
-
+ if (storageError) {
+ store.dispatch('pushGlobalNotice', { messageKey: 'errors.storage_unavailable', level: 'error' })
+ }
afterStoreSetup({ store, i18n })
})()
diff --git a/src/modules/api.js b/src/modules/api.js
index 748570e56..04ef6ab42 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -138,9 +138,6 @@ const api = {
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
},
- fetchAndUpdateNotifications (store) {
- store.state.backendInteractor.fetchAndUpdateNotifications({ store })
- },
// Follow requests
startFetchingFollowRequests (store) {
diff --git a/src/modules/interface.js b/src/modules/interface.js
index eeebd65eb..e31630fcb 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -14,7 +14,8 @@ const defaultState = {
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
)
},
- mobileLayout: false
+ mobileLayout: false,
+ globalNotices: []
}
const interfaceMod = {
@@ -58,6 +59,12 @@ const interfaceMod = {
if (!state.settingsModalLoaded) {
state.settingsModalLoaded = true
}
+ },
+ pushGlobalNotice (state, notice) {
+ state.globalNotices.push(notice)
+ },
+ removeGlobalNotice (state, notice) {
+ state.globalNotices = state.globalNotices.filter(n => n !== notice)
}
},
actions: {
@@ -81,6 +88,28 @@ const interfaceMod = {
},
togglePeekSettingsModal ({ commit }) {
commit('togglePeekSettingsModal')
+ },
+ pushGlobalNotice (
+ { commit, dispatch },
+ {
+ messageKey,
+ messageArgs = {},
+ level = 'error',
+ timeout = 0
+ }) {
+ const notice = {
+ messageKey,
+ messageArgs,
+ level
+ }
+ if (timeout) {
+ setTimeout(() => dispatch('removeGlobalNotice', notice), timeout)
+ }
+ commit('pushGlobalNotice', notice)
+ return notice
+ },
+ removeGlobalNotice ({ commit }, notice) {
+ commit('removeGlobalNotice', notice)
}
}
}
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 073b15f1b..7fbf685cf 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -62,7 +62,8 @@ export const defaultState = () => ({
publicAndExternal: emptyTl(),
friends: emptyTl(),
tag: emptyTl(),
- dms: emptyTl()
+ dms: emptyTl(),
+ bookmarks: emptyTl()
}
})
@@ -163,8 +164,7 @@ const removeStatusFromGlobalStorage = (state, status) => {
}
}
-const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {},
- noIdUpdate = false, userId }) => {
+const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId, pagination = {} }) => {
// Sanity check
if (!isArray(statuses)) {
return false
@@ -173,8 +173,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
const allStatuses = state.allStatuses
const timelineObject = state.timelines[timeline]
- const maxNew = statuses.length > 0 ? maxBy(statuses, 'id').id : 0
- const minNew = statuses.length > 0 ? minBy(statuses, 'id').id : 0
+ // Mismatch between API pagination and our internal minId/maxId tracking systems:
+ // pagination.maxId is the oldest of the returned statuses when fetching older,
+ // and pagination.minId is the newest when fetching newer. The names come directly
+ // from the arguments they're supposed to be passed as for the next fetch.
+ const minNew = pagination.maxId || (statuses.length > 0 ? minBy(statuses, 'id').id : 0)
+ const maxNew = pagination.minId || (statuses.length > 0 ? maxBy(statuses, 'id').id : 0)
+
const newer = timeline && (maxNew > timelineObject.maxId || timelineObject.maxId === 0) && statuses.length > 0
const older = timeline && (minNew < timelineObject.minId || timelineObject.minId === 0) && statuses.length > 0
@@ -315,7 +320,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
})
// Keep the visible statuses sorted
- if (timeline) {
+ if (timeline && !(timeline === 'bookmarks')) {
sortTimeline(timelineObject)
}
}
@@ -463,6 +468,14 @@ export const mutations = {
newStatus.rebloggedBy.push(user)
}
},
+ setBookmarked (state, { status, value }) {
+ const newStatus = state.allStatusesObject[status.id]
+ newStatus.bookmarked = value
+ },
+ setBookmarkedConfirm (state, { status }) {
+ const newStatus = state.allStatusesObject[status.id]
+ newStatus.bookmarked = status.bookmarked
+ },
setDeleted (state, { status }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.deleted = true
@@ -515,6 +528,11 @@ export const mutations = {
queueFlush (state, { timeline, id }) {
state.timelines[timeline].flushMarker = id
},
+ queueFlushAll (state) {
+ Object.keys(state.timelines).forEach((timeline) => {
+ state.timelines[timeline].flushMarker = state.timelines[timeline].maxId
+ })
+ },
addRepeats (state, { id, rebloggedByUsers, currentUser }) {
const newStatus = state.allStatusesObject[id]
newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _)
@@ -585,8 +603,8 @@ export const mutations = {
const statuses = {
state: defaultState(),
actions: {
- addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId }) {
- commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId })
+ 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 })
@@ -661,9 +679,26 @@ const statuses = {
rootState.api.backendInteractor.unretweet({ id: status.id })
.then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser }))
},
+ bookmark ({ rootState, commit }, status) {
+ commit('setBookmarked', { status, value: true })
+ rootState.api.backendInteractor.bookmarkStatus({ id: status.id })
+ .then(status => {
+ commit('setBookmarkedConfirm', { status })
+ })
+ },
+ unbookmark ({ rootState, commit }, status) {
+ commit('setBookmarked', { status, value: false })
+ rootState.api.backendInteractor.unbookmarkStatus({ id: status.id })
+ .then(status => {
+ commit('setBookmarkedConfirm', { status })
+ })
+ },
queueFlush ({ rootState, commit }, { timeline, id }) {
commit('queueFlush', { timeline, id })
},
+ queueFlushAll ({ rootState, commit }) {
+ commit('queueFlushAll')
+ },
markNotificationsAsSeen ({ rootState, commit }) {
commit('markNotificationsAsSeen')
apiService.markNotificationsAsSeen({
diff --git a/src/modules/users.js b/src/modules/users.js
index 5e32bb49c..68d029315 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -1,6 +1,6 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import oauthApi from '../services/new_api/oauth.js'
-import { compact, map, each, merge, last, concat, uniq } from 'lodash'
+import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
import { set } from 'vue'
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
@@ -10,7 +10,7 @@ export const mergeOrAdd = (arr, obj, item) => {
const oldItem = obj[item.id]
if (oldItem) {
// We already have this, so only merge the new info.
- merge(oldItem, item)
+ mergeWith(oldItem, item, mergeArrayLength)
return { item: oldItem, new: false }
} else {
// This is a new item, prepare it
@@ -23,6 +23,13 @@ export const mergeOrAdd = (arr, obj, item) => {
}
}
+const mergeArrayLength = (oldValue, newValue) => {
+ if (isArray(oldValue) && isArray(newValue)) {
+ oldValue.length = newValue.length
+ return mergeWith(oldValue, newValue, mergeArrayLength)
+ }
+}
+
const getNotificationPermission = () => {
const Notification = window.Notification
@@ -116,7 +123,7 @@ export const mutations = {
},
setCurrentUser (state, user) {
state.lastLoginName = user.screen_name
- state.currentUser = merge(state.currentUser || {}, user)
+ state.currentUser = mergeWith(state.currentUser || {}, user, mergeArrayLength)
},
clearCurrentUser (state) {
state.currentUser = false
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index dfffc291c..c9ec88b76 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,5 +1,5 @@
import { each, map, concat, last, get } from 'lodash'
-import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
+import { parseStatus, parseUser, parseNotification, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
import { RegistrationError, StatusCodeError } from '../errors/errors'
/* eslint-env browser */
@@ -50,6 +50,7 @@ const MASTODON_USER_URL = '/api/v1/accounts'
const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
const MASTODON_TAG_TIMELINE_URL = tag => `/api/v1/timelines/tag/${tag}`
+const MASTODON_BOOKMARK_TIMELINE_URL = '/api/v1/bookmarks'
const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/'
const MASTODON_USER_MUTES_URL = '/api/v1/mutes/'
const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block`
@@ -58,6 +59,8 @@ const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute`
const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
const MASTODON_SUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/subscribe`
const MASTODON_UNSUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/unsubscribe`
+const MASTODON_BOOKMARK_STATUS_URL = id => `/api/v1/statuses/${id}/bookmark`
+const MASTODON_UNBOOKMARK_STATUS_URL = id => `/api/v1/statuses/${id}/unbookmark`
const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
const MASTODON_VOTE_URL = id => `/api/v1/polls/${id}/votes`
@@ -498,7 +501,8 @@ const fetchTimeline = ({
until = false,
userId = false,
tag = false,
- withMuted = false
+ withMuted = false,
+ replyVisibility = 'all'
}) => {
const timelineUrls = {
public: MASTODON_PUBLIC_TIMELINE,
@@ -509,7 +513,8 @@ const fetchTimeline = ({
user: MASTODON_USER_TIMELINE_URL,
media: MASTODON_USER_TIMELINE_URL,
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
- tag: MASTODON_TAG_TIMELINE_URL
+ tag: MASTODON_TAG_TIMELINE_URL,
+ bookmarks: MASTODON_BOOKMARK_TIMELINE_URL
}
const isNotifications = timeline === 'notifications'
const params = []
@@ -538,9 +543,12 @@ const fetchTimeline = ({
if (timeline === 'public' || timeline === 'publicAndExternal') {
params.push(['only_media', false])
}
- if (timeline !== 'favorites') {
+ if (timeline !== 'favorites' && timeline !== 'bookmarks') {
params.push(['with_muted', withMuted])
}
+ if (replyVisibility !== 'all') {
+ params.push(['reply_visibility', replyVisibility])
+ }
params.push(['limit', 20])
@@ -548,16 +556,20 @@ const fetchTimeline = ({
url += `?${queryString}`
let status = ''
let statusText = ''
+ let pagination = {}
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => {
status = data.status
statusText = data.statusText
+ pagination = parseLinkHeaderPagination(data.headers.get('Link'), {
+ flakeId: timeline !== 'bookmarks' && timeline !== 'notifications'
+ })
return data
})
.then((data) => data.json())
.then((data) => {
if (!data.error) {
- return data.map(isNotifications ? parseNotification : parseStatus)
+ return { data: data.map(isNotifications ? parseNotification : parseStatus), pagination }
} else {
data.status = status
data.statusText = statusText
@@ -608,6 +620,22 @@ const unretweet = ({ id, credentials }) => {
.then((data) => parseStatus(data))
}
+const bookmarkStatus = ({ id, credentials }) => {
+ return promisedRequest({
+ url: MASTODON_BOOKMARK_STATUS_URL(id),
+ headers: authHeaders(credentials),
+ method: 'POST'
+ })
+}
+
+const unbookmarkStatus = ({ id, credentials }) => {
+ return promisedRequest({
+ url: MASTODON_UNBOOKMARK_STATUS_URL(id),
+ headers: authHeaders(credentials),
+ method: 'POST'
+ })
+}
+
const postStatus = ({
credentials,
status,
@@ -1146,6 +1174,8 @@ const apiService = {
unfavorite,
retweet,
unretweet,
+ bookmarkStatus,
+ unbookmarkStatus,
postStatus,
deleteStatus,
uploadMedia,
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index e1c32860d..45e6bd0e1 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -12,10 +12,6 @@ const backendInteractorService = credentials => ({
return notificationsFetcher.startFetching({ store, credentials })
},
- fetchAndUpdateNotifications ({ store }) {
- return notificationsFetcher.fetchAndUpdate({ store, credentials })
- },
-
startFetchingFollowRequests ({ store }) {
return followRequestFetcher.startFetching({ store, credentials })
},
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 3bdb92f3f..ec83c02a9 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -1,4 +1,5 @@
import escape from 'escape-html'
+import parseLinkHeader from 'parse-link-header'
import { isStatusNotification } from '../notification_utils/notification_utils.js'
const qvitterStatusType = (status) => {
@@ -232,6 +233,8 @@ export const parseStatus = (data) => {
output.repeated = data.reblogged
output.repeat_num = data.reblogs_count
+ output.bookmarked = data.bookmarked
+
output.type = data.reblog ? 'retweet' : 'status'
output.nsfw = data.sensitive
@@ -248,6 +251,7 @@ export const parseStatus = (data) => {
output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct
output.thread_muted = pleroma.thread_muted
output.emoji_reactions = pleroma.emoji_reactions
+ output.parent_visible = pleroma.parent_visible === undefined ? true : pleroma.parent_visible
} else {
output.text = data.content
output.summary = data.spoiler_text
@@ -381,3 +385,16 @@ const isNsfw = (status) => {
const nsfwRegex = /#nsfw/i
return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex)
}
+
+export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
+ const flakeId = opts.flakeId
+ const parsedLinkHeader = parseLinkHeader(linkHeader)
+ if (!parsedLinkHeader) return
+ const maxId = parsedLinkHeader.next.max_id
+ const minId = parsedLinkHeader.prev.min_id
+
+ return {
+ maxId: flakeId ? maxId : parseInt(maxId, 10),
+ minId: flakeId ? minId : parseInt(minId, 10)
+ }
+}
diff --git a/src/services/follow_request_fetcher/follow_request_fetcher.service.js b/src/services/follow_request_fetcher/follow_request_fetcher.service.js
index 786740b7e..93fac9bc0 100644
--- a/src/services/follow_request_fetcher/follow_request_fetcher.service.js
+++ b/src/services/follow_request_fetcher/follow_request_fetcher.service.js
@@ -4,6 +4,7 @@ const fetchAndUpdate = ({ store, credentials }) => {
return apiService.fetchFollowRequests({ credentials })
.then((requests) => {
store.commit('setFollowRequests', requests)
+ store.commit('addNewUsers', requests)
}, () => {})
.catch(() => {})
}
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index 64499a1b6..d282074ad 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -27,21 +27,25 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
}
const result = fetchNotifications({ store, args, older })
- // load unread notifications repeatedly to provide consistency between browser tabs
+ // If there's any unread notifications, try fetch notifications since
+ // the newest read notification to check if any of the unread notifs
+ // have changed their 'seen' state (marked as read in another session), so
+ // we can update the state in this session to mark them as read as well.
+ // The normal maxId-check does not tell if older notifications have changed
const notifications = timelineData.data
const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id)
- if (readNotifsIds.length) {
+ const numUnseenNotifs = notifications.length - readNotifsIds.length
+ if (numUnseenNotifs > 0) {
args['since'] = Math.max(...readNotifsIds)
fetchNotifications({ store, args, older })
}
-
return result
}
}
const fetchNotifications = ({ store, args, older }) => {
return apiService.fetchTimeline(args)
- .then((notifications) => {
+ .then(({ data: notifications }) => {
update({ store, notifications, older })
return notifications
}, () => store.dispatch('setNotificationsError', { value: true }))
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index b577cfab8..6b25cd6f6 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -34,7 +34,8 @@ export const DEFAULT_OPACITY = {
alert: 0.5,
input: 0.5,
faint: 0.5,
- underlay: 0.15
+ underlay: 0.15,
+ alertPopup: 0.95
}
/** SUBJECT TO CHANGE IN THE FUTURE, this is all beta
@@ -627,6 +628,39 @@ export const SLOT_INHERITANCE = {
textColor: true
},
+ alertPopupError: {
+ depends: ['alertError'],
+ opacity: 'alertPopup'
+ },
+ alertPopupErrorText: {
+ depends: ['alertErrorText'],
+ layer: 'popover',
+ variant: 'alertPopupError',
+ textColor: true
+ },
+
+ alertPopupWarning: {
+ depends: ['alertWarning'],
+ opacity: 'alertPopup'
+ },
+ alertPopupWarningText: {
+ depends: ['alertWarningText'],
+ layer: 'popover',
+ variant: 'alertPopupWarning',
+ textColor: true
+ },
+
+ alertPopupNeutral: {
+ depends: ['alertNeutral'],
+ opacity: 'alertPopup'
+ },
+ alertPopupNeutralText: {
+ depends: ['alertNeutralText'],
+ layer: 'popover',
+ variant: 'alertPopupNeutral',
+ textColor: true
+ },
+
badgeNotification: '--cRed',
badgeNotificationText: {
depends: ['text', 'badgeNotification'],
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index c6b28ad58..214294eb5 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -2,7 +2,7 @@ import { camelCase } from 'lodash'
import apiService from '../api/api.service.js'
-const update = ({ store, statuses, timeline, showImmediately, userId }) => {
+const update = ({ store, statuses, timeline, showImmediately, userId, pagination }) => {
const ccTimeline = camelCase(timeline)
store.dispatch('setError', { value: false })
@@ -12,7 +12,8 @@ const update = ({ store, statuses, timeline, showImmediately, userId }) => {
timeline: ccTimeline,
userId,
statuses,
- showImmediately
+ showImmediately,
+ pagination
})
}
@@ -30,7 +31,8 @@ const fetchAndUpdate = ({
const rootState = store.rootState || store.state
const { getters } = store
const timelineData = rootState.statuses.timelines[camelCase(timeline)]
- const hideMutedPosts = getters.mergedConfig.hideMutedPosts
+ const { hideMutedPosts, replyVisibility } = getters.mergedConfig
+ const loggedIn = !!rootState.users.currentUser
if (older) {
args['until'] = until || timelineData.minId
@@ -41,20 +43,23 @@ const fetchAndUpdate = ({
args['userId'] = userId
args['tag'] = tag
args['withMuted'] = !hideMutedPosts
+ if (loggedIn) args['replyVisibility'] = replyVisibility
const numStatusesBeforeFetch = timelineData.statuses.length
return apiService.fetchTimeline(args)
- .then((statuses) => {
- if (statuses.error) {
- store.dispatch('setErrorData', { value: statuses })
+ .then(response => {
+ if (response.error) {
+ store.dispatch('setErrorData', { value: response })
return
}
+
+ const { data: statuses, pagination } = response
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
+ update({ store, statuses, timeline, showImmediately, userId, pagination })
+ return { statuses, pagination }
}, () => store.dispatch('setError', { value: true }))
}
diff --git a/static/fontello.json b/static/fontello.json
old mode 100755
new mode 100644
index ac3f0a18c..6083c0bfa
--- a/static/fontello.json
+++ b/static/fontello.json
@@ -375,6 +375,18 @@
"css": "download",
"code": 59429,
"src": "fontawesome"
+ },
+ {
+ "uid": "f04a5d24e9e659145b966739c4fde82a",
+ "css": "bookmark",
+ "code": 59430,
+ "src": "fontawesome"
+ },
+ {
+ "uid": "2f5ef6f6b7aaebc56458ab4e865beff5",
+ "css": "bookmark-empty",
+ "code": 61591,
+ "src": "fontawesome"
}
]
}
\ No newline at end of file
diff --git a/test/unit/specs/modules/users.spec.js b/test/unit/specs/modules/users.spec.js
index 670acfc82..dfa5684d1 100644
--- a/test/unit/specs/modules/users.spec.js
+++ b/test/unit/specs/modules/users.spec.js
@@ -18,6 +18,42 @@ describe('The users module', () => {
expect(state.users).to.eql([user])
expect(state.users[0].name).to.eql('Dude')
})
+
+ it('merging array field in new information for old users', () => {
+ const state = cloneDeep(defaultState)
+ const user = {
+ id: '1',
+ fields: [
+ { name: 'Label 1', value: 'Content 1' }
+ ]
+ }
+ const firstModUser = {
+ id: '1',
+ fields: [
+ { name: 'Label 2', value: 'Content 2' },
+ { name: 'Label 3', value: 'Content 3' }
+ ]
+ }
+ const secondModUser = {
+ id: '1',
+ fields: [
+ { name: 'Label 4', value: 'Content 4' }
+ ]
+ }
+
+ mutations.addNewUsers(state, [user])
+ expect(state.users[0].fields).to.have.length(1)
+ expect(state.users[0].fields[0].name).to.eql('Label 1')
+
+ mutations.addNewUsers(state, [firstModUser])
+ expect(state.users[0].fields).to.have.length(2)
+ expect(state.users[0].fields[0].name).to.eql('Label 2')
+ expect(state.users[0].fields[1].name).to.eql('Label 3')
+
+ mutations.addNewUsers(state, [secondModUser])
+ expect(state.users[0].fields).to.have.length(1)
+ expect(state.users[0].fields[0].name).to.eql('Label 4')
+ })
})
describe('findUser', () => {
diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
index ccb57942c..e1f7a958f 100644
--- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
+++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
@@ -1,4 +1,4 @@
-import { parseStatus, parseUser, parseNotification, addEmojis } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
+import { parseStatus, parseUser, parseNotification, addEmojis, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
import mastoapidata from '../../../../fixtures/mastoapi.json'
import qvitterapidata from '../../../../fixtures/statuses.json'
@@ -383,4 +383,24 @@ describe('API Entities normalizer', () => {
expect(result).to.include('title=\':[a-z] {|}*:\'')
})
})
+
+ describe('Link header pagination', () => {
+ it('Parses min and max ids as integers', () => {
+ const linkHeader = '; rel="next", ; rel="prev"'
+ const result = parseLinkHeaderPagination(linkHeader)
+ expect(result).to.eql({
+ 'maxId': 861676,
+ 'minId': 861741
+ })
+ })
+
+ it('Parses min and max ids as flakes', () => {
+ const linkHeader = '; rel="next", ; rel="prev"'
+ const result = parseLinkHeaderPagination(linkHeader, { flakeId: true })
+ expect(result).to.eql({
+ 'maxId': '9waQx5IIS48qVue2Ai',
+ 'minId': '9wi61nIPnfn674xgie'
+ })
+ })
+ })
})
diff --git a/yarn.lock b/yarn.lock
index f05b00b15..093168631 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5751,6 +5751,13 @@ parse-json@^4.0.0:
error-ex "^1.3.1"
json-parse-better-errors "^1.0.1"
+parse-link-header@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/parse-link-header/-/parse-link-header-1.0.1.tgz#bedfe0d2118aeb84be75e7b025419ec8a61140a7"
+ integrity sha1-vt/g0hGK64S+deewJUGeyKYRQKc=
+ dependencies:
+ xtend "~4.0.1"
+
parseqs@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"