diff options
author | Maxence Lange <maxence@artificial-owl.com> | 2023-04-11 16:52:17 -0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-11 16:52:17 -0100 |
commit | 6d945ff03c6a381c5fcf2380044f7f72b3336481 (patch) | |
tree | 0ea9340559c14732ce405e00d455ba284cd5ae97 | |
parent | c71ed30f2f3cb55212e106b67447d619aa50b348 (diff) | |
parent | cb47afc124e770b4d599d71bdd03422bb146768c (diff) |
Merge pull request #1729 from nextcloud/artonge/fix/again_some_fixes
Fix blocking issues
-rw-r--r-- | src/components/Composer/Composer.vue | 2 | ||||
-rw-r--r-- | src/components/TimelineEntry.vue | 99 | ||||
-rw-r--r-- | src/components/TimelinePost.vue | 53 | ||||
-rw-r--r-- | src/store/timeline.js | 225 | ||||
-rw-r--r-- | src/views/Timeline.vue | 7 | ||||
-rw-r--r-- | src/views/TimelineSinglePost.vue | 14 |
6 files changed, 257 insertions, 143 deletions
diff --git a/src/components/Composer/Composer.vue b/src/components/Composer/Composer.vue index e7111f11..e0a77ecc 100644 --- a/src/components/Composer/Composer.vue +++ b/src/components/Composer/Composer.vue @@ -378,7 +378,7 @@ export default { this.updateStatusContent() }, keyup(event) { - if (event.shiftKey || event.ctrlKey) { + if (event.ctrlKey) { this.createPost(event) } }, diff --git a/src/components/TimelineEntry.vue b/src/components/TimelineEntry.vue index 2c641ebe..80812f1a 100644 --- a/src/components/TimelineEntry.vue +++ b/src/components/TimelineEntry.vue @@ -1,10 +1,24 @@ <template> - <li :class="['timeline-entry', hasHeader ? 'with-header' : '']"> - <div v-if="isNotification" class="notification"> - <Bell :size="22" /> - <span class="notification-action"> + <component :is="element" class="timeline-entry" :class="{ 'notification': isNotification, 'with-header': hasHeader }"> + <div v-if="isNotification" class="notification__header"> + <span class="notification__summary"> + <img :src="notification.account.avatar"> + <Heart v-if="notification.type === 'favourite'" :size="16" /> + <Repeat v-if="notification.type === 'reblog'" :size="16" /> {{ actionSummary }} </span> + <span class="notification__details"> + <router-link :to="{ name: 'single-post', params: { + account: item.account.display_name, + id: notification.status.id, + type: 'single-post', + } }" + :data-timestamp="notification.created_at" + class="post-timestamp live-relative-timestamp" + :title="notificationFormattedDate"> + {{ notificationRelativeTimestamp }} + </router-link> + </span> </div> <template v-else-if="isBoost"> <div class="container-icon-boost boost"> @@ -24,18 +38,22 @@ :item="item.account" /> <template v-else> <div class="wrapper"> - <TimelineAvatar class="entry__avatar" :item="entryContent" /> + <TimelineAvatar v-if="!isNotification" class="entry__avatar" :item="entryContent" /> <TimelinePost class="entry__content" :item="entryContent" :type="type" /> </div> </template> - </li> + </component> </template> <script> import Bell from 'vue-material-design-icons/Bell.vue' +import Repeat from 'vue-material-design-icons/Repeat.vue' +import Reply from 'vue-material-design-icons/Reply.vue' +import Heart from 'vue-material-design-icons/Heart.vue' import { translate } from '@nextcloud/l10n' +import moment from '@nextcloud/moment' import TimelinePost from './TimelinePost.vue' import TimelineAvatar from './TimelineAvatar.vue' import UserEntry from './UserEntry.vue' @@ -48,6 +66,9 @@ export default { TimelineAvatar, UserEntry, Bell, + Repeat, + Reply, + Heart, }, props: { /** @type {import('vue').PropType<import('../types/Mastodon.js').Status|import('../types/Mastodon.js').Notification>} */ @@ -59,6 +80,10 @@ export default { type: String, required: true, }, + element: { + type: String, + default: 'li', + }, }, computed: { /** @@ -77,6 +102,14 @@ export default { isNotification() { return this.item.type !== undefined }, + /** @return {string} */ + notificationFormattedDate() { + return moment(this.notification.created_at).format('LLL') + }, + /** @return {string} */ + notificationRelativeTimestamp() { + return moment(this.notification.created_at).fromNow() + }, /** @return {boolean} */ isBoost() { return this.status.reblog !== null @@ -137,21 +170,61 @@ export default { } .notification { - display: flex; - padding-left: 2rem; - gap: 0.2rem; - margin-top: 1rem; + border-bottom: 1px solid var(--color-border); + + &__header { + display: flex; + gap: 0.2rem; + margin-top: 1rem; + } - &-action { + &__summary { flex-grow: 1; display: inline-block; grid-row: 1; grid-column: 2; color: var(--color-text-lighter); + position: relative; + margin-bottom: 8px; + + img { + width: 32px; + border-radius: 50%; + overflow: hidden; + margin-right: 3px; + vertical-align: middle; + margin-top: -1px; + margin-right: 8px; + } + + .material-design-icon { + position: absolute; + top: 16px; + left: 20px; + padding: 2px; + background: var(--color-main-background); + border-radius: 50%; + border: 1px solid var(--color-background-dark); + } + + } + + &__details a { + color: var(--color-text-lighter); + + &:hover { + text-decoration: underline; + } } - .bell-icon { - opacity: .5; + :deep(.post-header) { + .post-visibility { + display: none; + } + + .post-timestamp { + display: none; + } } } diff --git a/src/components/TimelinePost.vue b/src/components/TimelinePost.vue index 5d504788..1b23b79b 100644 --- a/src/components/TimelinePost.vue +++ b/src/components/TimelinePost.vue @@ -14,16 +14,16 @@ </span> </router-link> </div> - <VisibilityIcon v-if="visibility" - :title="visibility.text" - class="post-visibility" - :visibility="visibility.id" /> <a :data-timestamp="timestamp" class="post-timestamp live-relative-timestamp" :title="formattedDate" @click="getSinglePostTimeline"> {{ relativeTimestamp }} </a> + <VisibilityIcon v-if="visibility" + :title="visibility.text" + class="post-visibility" + :visibility="visibility.id" /> </div> <div v-if="item.content" class="post-message"> <MessageContent :item="item" /> @@ -33,7 +33,7 @@ <PostAttachment v-if="hasAttachments" :attachments="item.media_attachments || []" /> <div v-if="$route && $route.params.type !== 'notifications' && !serverData.public" class="post-actions"> <NcButton :title="t('social', 'Reply')" - type="tertiary-no-background" + type="tertiary" @click="reply"> <template #icon> <Reply :size="20" /> @@ -46,7 +46,7 @@ </NcButton> <NcButton v-if="item.visibility === 'public' || item.visibility === 'unlisted'" :title="t('social', 'Boost')" - type="tertiary-no-background" + type="tertiary" @click="boost"> <template #icon> <Repeat :size="20" :fill-color="isBoosted ? 'var(--color-primary)' : 'var(--color-main-text)'" /> @@ -59,7 +59,7 @@ </NcButton> <NcButton v-if="!isLiked" :title="t('social', 'Like')" - type="tertiary-no-background" + type="tertiary" @click="like"> <template #icon> <HeartOutline :size="20" /> @@ -72,7 +72,7 @@ </NcButton> <NcButton v-if="isLiked" :title="t('social', 'Undo Like')" - type="tertiary-no-background" + type="tertiary" @click="like"> <template #icon> <Heart :size="20" :fill-color="'var(--color-error)'" /> @@ -208,24 +208,18 @@ export default { getSinglePostTimeline(e) { // Display internal or external post if (!this.isLocal) { - // TODO - fix - if (this.type === 'Note') { - window.open(this.item.id) - } else if (this.type === 'Announce') { - window.open(this.item.object) - } else { - logger.warn("Don't know what to do with posts of type " + this.type, { post: this.item }) - } - } else { - this.$router.push({ - name: 'single-post', - params: { - account: this.item.account.display_name, - id: this.item.id, - type: 'single-post', - }, - }) + logger.warn("Don't know what to do with posts of type " + this.type, { post: this.item }) + return } + + this.$router.push({ + name: 'single-post', + params: { + account: this.item.account.display_name, + id: this.item.id, + type: 'single-post', + }, + }) }, userDisplayName(actorInfo) { return actorInfo.name !== '' ? actorInfo.name : actorInfo.preferredUsername @@ -236,7 +230,7 @@ export default { }, boost() { const params = { - post: this.item, + status: this.item, parentAnnounce: this.reblog, } if (this.isBoosted) { @@ -250,7 +244,7 @@ export default { }, like() { const params = { - post: this.item, + status: this.item, parentAnnounce: this.reblog, } if (this.isLiked) { @@ -330,6 +324,11 @@ export default { margin-left: -13px; height: 44px; display: flex; + margin: 4px; + + .button-vue:hover { + background: var(--color-background-dark); + } .post-actions-more { position: relative; diff --git a/src/store/timeline.js b/src/store/timeline.js index 4b67ef99..e972c0a1 100644 --- a/src/store/timeline.js +++ b/src/store/timeline.js @@ -33,13 +33,17 @@ import logger from '../services/logger.js' const state = { /** - * @type {Object<string, import('../types/Mastodon.js').Status>} timeline - The posts' collection + * @type {Object<string, import('../types/Mastodon.js').Status>} List of locally known statuses */ - timeline: {}, + statuses: {}, /** - * @type {Object<string, import('../types/Mastodon.js').Status>} timeline - The parents posts' collection + * @type {string[]} timeline - The statuses' collection */ - parentsTimeline: {}, + timeline: [], + /** + * @type {string[]} parentsTimeline - The parents statuses' collection + */ + parentsTimeline: [], /** * @type {string} type - Timeline's type: 'home', 'single-post',... */ @@ -49,6 +53,7 @@ const state = { * @property {string} params.account ??? * @property {string} params.id * @property {string} params.type ??? + * @property {string?} params.singlePost ??? */ params: {}, /** @@ -68,26 +73,61 @@ const state = { const mutations = { /** * @param state + * @param {import ('../types/Mastodon.js').Status} status + */ + addToStatuses(state, status) { + Vue.set(state.statuses, status.id, status) + }, + /** + * @param state * @param {import ('../types/Mastodon.js').Status[]|import('../types/Mastodon.js').Context} data */ addToTimeline(state, data) { if (Array.isArray(data)) { - data.forEach((post) => Vue.set(state.timeline, post.id, post)) + data.forEach(status => Vue.set(state.statuses, status.id, status)) + data + .filter(status => state.timeline.indexOf(status.id) === -1) + .forEach(status => state.timeline.push(status.id)) } else { - data.descendants.forEach((post) => Vue.set(state.timeline, post.id, post)) - data.ancestors.forEach((post) => Vue.set(state.parentsTimeline, post.id, post)) + data.descendants.forEach(status => Vue.set(state.statuses, status.id, status)) + data.ancestors.forEach(status => Vue.set(state.statuses, status.id, status)) + + data.descendants + .filter(status => state.timeline.indexOf(status.id) === -1) + .forEach(status => state.timeline.push(status.id)) + data.ancestors + .filter(status => state.parentsTimeline.indexOf(status.id) === -1) + .forEach(status => state.parentsTimeline.push(status.id)) } }, /** * @param state - * @param {import('../types/Mastodon.js').Status} post + * @param {import ('../types/Mastodon.js').Status[]} data + */ + updateInTimelines(state, data) { + data.forEach((status) => { + if (state.statuses[status.id] !== undefined) { + Vue.set(state.statuses, status.id, status) + } + }) + }, + /** + * @param state + * @param {import('../types/Mastodon.js').Status} status */ - removePost(state, post) { - Vue.delete(state.timeline, post.id) + removeStatusf(state, status) { + const timelineIndex = state.timeline.indexOf(status.id) + if (timelineIndex !== -1) { + state.timeline.splice(timelineIndex, 1) + } + const parentsTimelineIndex = state.parentsTimeline.indexOf(status.id) + if (timelineIndex !== -1) { + state.parentsTimeline.splice(parentsTimelineIndex, 1) + } }, resetTimeline(state) { - state.timeline = {} - state.parentsTimeline = {} + state.timeline = [] + state.parentsTimeline = [] }, /** * @param state @@ -116,53 +156,41 @@ const mutations = { /** * @param state * @param {object} root0 - * @param {import('../types/Mastodon.js').Status} root0.post + * @param {import('../types/Mastodon.js').Status} root0.status */ - likePost(state, { post }) { - if (state.timeline[post.id] !== undefined) { - Vue.set(state.timeline[post.id], 'favourited', true) - } - if (post.reblog !== null && state.timeline[post.reblog.id] !== undefined) { - Vue.set(state.timeline[post.reblog.id], 'favourited', true) + likeStatus(state, { status }) { + if (state.statuses[status.id] !== undefined) { + Vue.set(state.statuses[status.id], 'favourited', true) } }, /** * @param state * @param {object} root0 - * @param {import('../types/Mastodon.js').Status} root0.post + * @param {import('../types/Mastodon.js').Status} root0.status */ - unlikePost(state, { post }) { - if (state.timeline[post.id] !== undefined) { - Vue.set(state.timeline[post.id], 'favourited', false) - } - if (post.reblog !== null && state.timeline[post.reblog.id] !== undefined) { - Vue.set(state.timeline[post.reblog.id], 'favourited', false) + unlikeStatus(state, { status }) { + if (state.statuses[status.id] !== undefined) { + Vue.set(state.statuses[status.id], 'favourited', false) } }, /** * @param state * @param {object} root0 - * @param {import('../types/Mastodon.js').Status} root0.post + * @param {import('../types/Mastodon.js').Status} root0.status */ - boostPost(state, { post }) { - if (state.timeline[post.id] !== undefined) { - Vue.set(state.timeline[post.id], 'reblogged', true) - } - if (post.reblog !== null && state.timeline[post.reblog.id] !== undefined) { - Vue.set(state.timeline[post.reblog.id], 'reblogged', true) + boostStatus(state, { status }) { + if (state.statuses[status.id] !== undefined) { + Vue.set(state.statuses[status.id], 'reblogged', true) } }, /** * @param state * @param {object} root0 - * @param {import('../types/Mastodon.js').Status} root0.post + * @param {import('../types/Mastodon.js').Status} root0.status */ - unboostPost(state, { post }) { - if (state.timeline[post.id] !== undefined) { - Vue.set(state.timeline[post.id], 'reblogged', false) - } - if (post.reblog !== null && state.timeline[post.reblog.id] !== undefined) { - Vue.set(state.timeline[post.reblog.id], 'reblogged', false) + unboostStatus(state, { status }) { + if (state.statuses[status.id] !== undefined) { + Vue.set(state.statuses[status.id], 'reblogged', false) } }, } @@ -173,23 +201,24 @@ const getters = { return state.composerDisplayStatus }, getTimeline(state) { - return Object.values(state.timeline).sort(function(a, b) { - return new Date(b.created_at).getTime() - new Date(a.created_at).getTime() - }) + return state.timeline + .map(statusId => state.statuses[statusId]) + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) }, getParentsTimeline(state) { - return Object.values(state.parentsTimeline).sort(function(a, b) { - return new Date(b.created_at).getTime() - new Date(a.created_at).getTime() - }) + return state.parentsTimeline + .map(statusId => state.statuses[statusId]) + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) + }, + getSinglePost(state) { + return state.statuses[state.params.singlePost] }, getPostFromTimeline(state) { - return (postId) => { - if (state.timeline[postId] !== undefined) { - return state.timeline[postId] - } else if (state.parentsTimeline[postId] !== undefined) { - return state.parentsTimeline[postId] + return (/** @type {string} */ statusId) => { + if (state.statuses[statusId] !== undefined) { + return state.statuses[statusId] } else { - logger.warn('Could not find post in timeline', { postId }) + logger.warn('Could not find status in timeline', { statusId }) } } }, @@ -234,108 +263,108 @@ const actions = { }, /** * @param context - * @param {import('../types/Mastodon.js').Status} post + * @param {import('../types/Mastodon.js').Status} status */ - async post(context, post) { + async post(context, status) { try { - const { data } = await axios.post(generateUrl('apps/social/api/v1/statuses'), post) + const { data } = await axios.post(generateUrl('apps/social/api/v1/statuses'), status) logger.info('Post created', data.id) } catch (error) { - showError('Failed to create a post') - logger.error('Failed to create a post', { error }) + showError('Failed to create a status') + logger.error('Failed to create a status', { error }) } }, /** * @param context - * @param {import('../types/Mastodon.js').Status} post + * @param {import('../types/Mastodon.js').Status} status */ - async postDelete(context, post) { + async postDelete(context, status) { try { - context.commit('removePost', post) - const response = await axios.delete(generateUrl(`apps/social/api/v1/post?id=${post.uri}`)) + context.commit('removeStatusf', status) + const response = await axios.delete(generateUrl(`apps/social/api/v1/post?id=${status.uri}`)) logger.info('Post deleted with token ' + response.data.result.token) } catch (error) { - context.commit('addToTimeline', [post]) - showError('Failed to delete the post') - logger.error('Failed to delete the post', { error }) + context.commit('updateInTimelines', [status]) + showError('Failed to delete the status') + logger.error('Failed to delete the status', { error }) } }, /** * @param context * @param {object} root0 - * @param {import('../types/Mastodon.js').Status} root0.post + * @param {import('../types/Mastodon.js').Status} root0.status */ - async postLike(context, { post }) { + async postLike(context, { status }) { try { - context.commit('likePost', { post }) - const response = await axios.post(generateUrl(`apps/social/api/v1/statuses/${post.id}/favourite`)) + context.commit('likeStatus', { status }) + const response = await axios.post(generateUrl(`apps/social/api/v1/statuses/${status.id}/favourite`)) logger.info('Post liked') - context.commit('addToTimeline', [response.data]) + context.commit('updateInTimelines', [response.data]) return response } catch (error) { - context.commit('unlikePost', { post }) - showError('Failed to like post') - logger.error('Failed to like post', { error }) + context.commit('unlikeStatus', { status }) + showError('Failed to like status') + logger.error('Failed to like status', { error }) } }, /** * @param context * @param {object} root0 - * @param {import('../types/Mastodon.js').Status} root0.post + * @param {import('../types/Mastodon.js').Status} root0.status */ - async postUnlike(context, { post }) { + async postUnlike(context, { status }) { try { - // Remove post from list if we are in the 'liked' timeline + // Remove status from list if we are in the 'liked' timeline if (state.type === 'liked') { - context.commit('removePost', post) + context.commit('removeStatusf', status) } - context.commit('unlikePost', { post }) - const response = await axios.post(generateUrl(`apps/social/api/v1/statuses/${post.id}/unfavourite`)) + context.commit('unlikeStatus', { status }) + const response = await axios.post(generateUrl(`apps/social/api/v1/statuses/${status.id}/unfavourite`)) logger.info('Post unliked') - context.commit('addToTimeline', [response.data]) + context.commit('updateInTimelines', [response.data]) return response } catch (error) { - // Readd post from list if we are in the 'liked' timeline + // Readd status from list if we are in the 'liked' timeline if (state.type === 'liked') { - context.commit('addToTimeline', [post]) + context.commit('addToTimeline', [status]) } - context.commit('likePost', { post }) - showError('Failed to unlike post') - logger.error('Failed to unlike post', { error }) + context.commit('likeStatus', { status }) + showError('Failed to unlike status') + logger.error('Failed to unlike status', { error }) } }, /** * @param context * @param {object} root0 - * @param {import('../types/Mastodon.js').Status} root0.post + * @param {import('../types/Mastodon.js').Status} root0.status */ - async postBoost(context, { post }) { + async postBoost(context, { status }) { try { - context.commit('boostPost', { post }) - const response = await axios.post(generateUrl(`apps/social/api/v1/statuses/${post.id}/reblog`)) + context.commit('boostStatus', { status }) + const response = await axios.post(generateUrl(`apps/social/api/v1/statuses/${status.id}/reblog`)) logger.info('Post boosted') - context.commit('addToTimeline', [response.data]) + context.commit('updateInTimelines', [response.data]) return response } catch (error) { - context.commit('unboostPost', { post }) - showError('Failed to create a boost post') - logger.error('Failed to create a boost post', { error }) + context.commit('unboostStatus', { status }) + showError('Failed to create a boost status') + logger.error('Failed to create a boost status', { error }) } }, /** * @param context * @param {object} root0 - * @param {import('../types/Mastodon.js').Status} root0.post + * @param {import('../types/Mastodon.js').Status} root0.status */ - async postUnBoost(context, { post }) { + async postUnBoost(context, { status }) { try { - context.commit('unboostPost', { post }) - const response = await axios.post(generateUrl(`apps/social/api/v1/statuses/${post.id}/unreblog`)) + context.commit('unboostStatus', { status }) + const response = await axios.post(generateUrl(`apps/social/api/v1/statuses/${status.id}/unreblog`)) logger.info('Boost deleted') - context.commit('addToTimeline', [response.data]) + context.commit('updateInTimelines', [response.data]) return response } catch (error) { - context.commit('boostPost', { post }) + context.commit('boostStatus', { status }) showError('Failed to delete the boost') logger.error('Failed to delete the boost', { error }) } diff --git a/src/views/Timeline.vue b/src/views/Timeline.vue index 2ee0fe5c..2a8958c0 100644 --- a/src/views/Timeline.vue +++ b/src/views/Timeline.vue @@ -25,10 +25,17 @@ </div> </div> </transition> + <Composer v-if="type !== 'notifications' && type !== 'single-post'" :default-visibility="type === 'direct' ? 'direct' : undefined" /> + <h2 v-if="type === 'tags'"> #{{ $route.params.tag }} </h2> + + <h2 v-if="type === 'notifications'"> + {{ t('social', 'Notifications') }} + </h2> + <TimelineList :type="type" /> </div> </template> diff --git a/src/views/TimelineSinglePost.vue b/src/views/TimelineSinglePost.vue index af9bcf90..f7e4b45f 100644 --- a/src/views/TimelineSinglePost.vue +++ b/src/views/TimelineSinglePost.vue @@ -7,8 +7,9 @@ :reverse-order="true" /> <TimelineEntry ref="mainPost" class="main-post" - :item="mainPost" - type="single-post" /> + :item="singlePost" + type="single-post" + element="div" /> <TimelineList v-if="timeline" class="descendants" :type="$route.params.type" /> </div> </template> @@ -36,11 +37,14 @@ export default { ], data() { return { - mainPost: {}, uid: this.$route.params.account, } }, computed: { + /** @return {Status?} */ + singlePost() { + return this.$store.getters.getSinglePost + }, /** * @description Tells whether Composer shall be displayed or not * @return {boolean} @@ -79,15 +83,17 @@ export default { }, }, async beforeMount() { - this.mainPost = this.$store.getters.getPostFromTimeline(this.$route.params.id) || loadState('social', 'item') + const singlePost = this.$store.getters.getPostFromTimeline(this.$route.params.id) || loadState('social', 'item') // Fetch single post timeline + this.$store.commit('addToStatuses', singlePost) this.$store.dispatch('changeTimelineType', { type: 'single-post', params: { account: this.account, id: this.$route.params.id, type: 'single-post', + singlePost: this.$route.params.id || loadState('social', 'item').id, }, }) |