forked from AkkomaGang/akkoma-fe
fix conflicts, make subject update the preview
This commit is contained in:
commit
afdc3f96f0
44 changed files with 575 additions and 156 deletions
|
@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- 'Bot' settings option and badge
|
- 'Bot' settings option and badge
|
||||||
- Added profile meta data fields that can be set in profile settings
|
- Added profile meta data fields that can be set in profile settings
|
||||||
- Added status preview option to preview your statuses before posting
|
- Added status preview option to preview your statuses before posting
|
||||||
|
- When a post is a reply to an unavailable post, the 'Reply to'-text has a strike-through style
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Registration page no longer requires email if the server is configured not to require it
|
- Registration page no longer requires email if the server is configured not to require it
|
||||||
|
@ -38,6 +39,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Subject field now appears disabled when posting
|
- Subject field now appears disabled when posting
|
||||||
- Fix status ellipsis menu being cut off in notifications column
|
- Fix status ellipsis menu being cut off in notifications column
|
||||||
- Fixed autocomplete sometimes not returning the right user when there's already some results
|
- Fixed autocomplete sometimes not returning the right user when there's already some results
|
||||||
|
- Reply filtering options in Settings -> Filtering now work again using filtering on server
|
||||||
|
- Don't show just blank-screen when cookies are disabled
|
||||||
|
|
||||||
## [2.0.3] - 2020-05-02
|
## [2.0.3] - 2020-05-02
|
||||||
### Fixed
|
### Fixed
|
||||||
|
@ -99,6 +102,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Ability to change user's email
|
- Ability to change user's email
|
||||||
- About page
|
- About page
|
||||||
- Added remote user redirect
|
- Added remote user redirect
|
||||||
|
- Bookmarks
|
||||||
### Changed
|
### Changed
|
||||||
- changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes
|
- changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
@ -8,8 +8,6 @@
|
||||||
>
|
>
|
||||||
> --Catbag
|
> --Catbag
|
||||||
|
|
||||||
Pleroma-FE user interface is modeled after Qvitter which is modeled after older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options.
|
|
||||||
|
|
||||||
## Posting, reading, basic functions.
|
## Posting, reading, basic functions.
|
||||||
|
|
||||||
After registering and logging in you're presented with your timeline in right column and new post form with timeline list and notifications in the left column.
|
After registering and logging in you're presented with your timeline in right column and new post form with timeline list and notifications in the left column.
|
||||||
|
|
8
docs/index.md
Normal file
8
docs/index.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# Introduction to Pleroma-FE
|
||||||
|
## What is Pleroma-FE?
|
||||||
|
|
||||||
|
Pleroma-FE is the default user-facing frontend for Pleroma. It's user interface is modeled after Qvitter which is modeled after an older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options.
|
||||||
|
|
||||||
|
## How can I use it?
|
||||||
|
|
||||||
|
If your instance uses Pleroma-FE, you can acces it by going to your instance (e.g. <https://pleroma.soykaf.com>). You can read more about it's basic functionality in the [Pleroma-FE User Guide](./USER_GUIDE.md). We also have [a guide for administrators](./CONFIGURATION.md) and for [hackers/contributors](./HACKING.md).
|
|
@ -22,6 +22,7 @@
|
||||||
"cropperjs": "^1.4.3",
|
"cropperjs": "^1.4.3",
|
||||||
"diff": "^3.0.1",
|
"diff": "^3.0.1",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
|
"parse-link-header": "^1.0.1",
|
||||||
"localforage": "^1.5.0",
|
"localforage": "^1.5.0",
|
||||||
"phoenix": "^1.3.0",
|
"phoenix": "^1.3.0",
|
||||||
"portal-vue": "^2.1.4",
|
"portal-vue": "^2.1.4",
|
||||||
|
|
|
@ -13,6 +13,7 @@ import MobilePostStatusButton from './components/mobile_post_status_button/mobil
|
||||||
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
||||||
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
||||||
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||||
|
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||||
import { windowWidth } from './services/window_utils/window_utils'
|
import { windowWidth } from './services/window_utils/window_utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -32,7 +33,8 @@ export default {
|
||||||
MobileNav,
|
MobileNav,
|
||||||
SettingsModal,
|
SettingsModal,
|
||||||
UserReportingModal,
|
UserReportingModal,
|
||||||
PostStatusModal
|
PostStatusModal,
|
||||||
|
GlobalNoticeList
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
mobileActivePanel: 'timeline',
|
mobileActivePanel: 'timeline',
|
||||||
|
|
|
@ -858,6 +858,10 @@ nav {
|
||||||
display: block;
|
display: block;
|
||||||
margin-right: 0.8em;
|
margin-right: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
margin-bottom: 7em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-multiple {
|
.select-multiple {
|
||||||
|
|
|
@ -128,6 +128,7 @@
|
||||||
<PostStatusModal />
|
<PostStatusModal />
|
||||||
<SettingsModal />
|
<SettingsModal />
|
||||||
<portal-target name="modal" />
|
<portal-target name="modal" />
|
||||||
|
<GlobalNoticeList />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import PublicTimeline from 'components/public_timeline/public_timeline.vue'
|
||||||
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
|
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
|
||||||
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
|
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
|
||||||
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
|
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
|
||||||
|
import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue'
|
||||||
import ConversationPage from 'components/conversation-page/conversation-page.vue'
|
import ConversationPage from 'components/conversation-page/conversation-page.vue'
|
||||||
import Interactions from 'components/interactions/interactions.vue'
|
import Interactions from 'components/interactions/interactions.vue'
|
||||||
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
||||||
|
@ -40,6 +41,7 @@ export default (store) => {
|
||||||
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
|
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
|
||||||
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||||
|
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
|
||||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||||
{ name: 'remote-user-profile-acct',
|
{ name: 'remote-user-profile-acct',
|
||||||
path: '/remote-users/(@?):username([^/@]+)@:hostname([^/@]+)',
|
path: '/remote-users/(@?):username([^/@]+)@:hostname([^/@]+)',
|
||||||
|
|
17
src/components/bookmark_timeline/bookmark_timeline.js
Normal file
17
src/components/bookmark_timeline/bookmark_timeline.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import Timeline from '../timeline/timeline.vue'
|
||||||
|
|
||||||
|
const Bookmarks = {
|
||||||
|
computed: {
|
||||||
|
timeline () {
|
||||||
|
return this.$store.state.statuses.timelines.bookmarks
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Timeline
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Bookmarks
|
9
src/components/bookmark_timeline/bookmark_timeline.vue
Normal file
9
src/components/bookmark_timeline/bookmark_timeline.vue
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<template>
|
||||||
|
<Timeline
|
||||||
|
:title="$t('nav.bookmarks')"
|
||||||
|
:timeline="timeline"
|
||||||
|
:timeline-name="'bookmarks'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./bookmark_timeline.js"></script>
|
|
@ -34,6 +34,16 @@ const ExtraButtons = {
|
||||||
navigator.clipboard.writeText(this.statusLink)
|
navigator.clipboard.writeText(this.statusLink)
|
||||||
.then(() => this.$emit('onSuccess'))
|
.then(() => this.$emit('onSuccess'))
|
||||||
.catch(err => this.$emit('onError', err.error.error))
|
.catch(err => this.$emit('onError', err.error.error))
|
||||||
|
},
|
||||||
|
bookmarkStatus () {
|
||||||
|
this.$store.dispatch('bookmark', { id: this.status.id })
|
||||||
|
.then(() => this.$emit('onSuccess'))
|
||||||
|
.catch(err => this.$emit('onError', err.error.error))
|
||||||
|
},
|
||||||
|
unbookmarkStatus () {
|
||||||
|
this.$store.dispatch('unbookmark', { id: this.status.id })
|
||||||
|
.then(() => this.$emit('onSuccess'))
|
||||||
|
.catch(err => this.$emit('onError', err.error.error))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -40,6 +40,22 @@
|
||||||
>
|
>
|
||||||
<i class="icon-pin" /><span>{{ $t("status.unpin") }}</span>
|
<i class="icon-pin" /><span>{{ $t("status.unpin") }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="!status.bookmarked"
|
||||||
|
class="dropdown-item dropdown-item-icon"
|
||||||
|
@click.prevent="bookmarkStatus"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
<i class="icon-bookmark-empty" /><span>{{ $t("status.bookmark") }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="status.bookmarked"
|
||||||
|
class="dropdown-item dropdown-item-icon"
|
||||||
|
@click.prevent="unbookmarkStatus"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
<i class="icon-bookmark" /><span>{{ $t("status.unbookmark") }}</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canDelete"
|
v-if="canDelete"
|
||||||
class="dropdown-item dropdown-item-icon"
|
class="dropdown-item dropdown-item-icon"
|
||||||
|
|
15
src/components/global_notice_list/global_notice_list.js
Normal file
15
src/components/global_notice_list/global_notice_list.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
const GlobalNoticeList = {
|
||||||
|
computed: {
|
||||||
|
notices () {
|
||||||
|
return this.$store.state.interface.globalNotices
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeNotice (notice) {
|
||||||
|
this.$store.dispatch('removeGlobalNotice', notice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GlobalNoticeList
|
77
src/components/global_notice_list/global_notice_list.vue
Normal file
77
src/components/global_notice_list/global_notice_list.vue
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<template>
|
||||||
|
<div class="global-notice-list">
|
||||||
|
<div
|
||||||
|
v-for="(notice, index) in notices"
|
||||||
|
:key="index"
|
||||||
|
class="alert global-notice"
|
||||||
|
:class="{ ['global-' + notice.level]: true }"
|
||||||
|
>
|
||||||
|
<div class="notice-message">
|
||||||
|
{{ $t(notice.messageKey, notice.messageArgs) }}
|
||||||
|
</div>
|
||||||
|
<i
|
||||||
|
class="button-icon icon-cancel"
|
||||||
|
@click="closeNotice(notice)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./global_notice_list.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.global-notice-list {
|
||||||
|
position: fixed;
|
||||||
|
top: 50px;
|
||||||
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1001;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.global-notice {
|
||||||
|
pointer-events: auto;
|
||||||
|
text-align: center;
|
||||||
|
width: 40em;
|
||||||
|
max-width: calc(100% - 3em);
|
||||||
|
display: flex;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
line-height: 2em;
|
||||||
|
.notice-message {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
i {
|
||||||
|
flex: 0 0;
|
||||||
|
width: 1.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-error {
|
||||||
|
background-color: var(--alertPopupError, $fallback--cRed);
|
||||||
|
color: var(--alertPopupErrorText, $fallback--text);
|
||||||
|
i {
|
||||||
|
color: var(--alertPopupErrorText, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-warning {
|
||||||
|
background-color: var(--alertPopupWarning, $fallback--cOrange);
|
||||||
|
color: var(--alertPopupWarningText, $fallback--text);
|
||||||
|
i {
|
||||||
|
color: var(--alertPopupWarningText, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-info {
|
||||||
|
background-color: var(--alertPopupNeutral, $fallback--fg);
|
||||||
|
color: var(--alertPopupNeutralText, $fallback--text);
|
||||||
|
i {
|
||||||
|
color: var(--alertPopupNeutralText, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -17,6 +17,11 @@
|
||||||
<i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }}
|
<i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="currentUser">
|
||||||
|
<router-link :to="{ name: 'bookmarks'}">
|
||||||
|
<i class="button-icon icon-bookmark" /> {{ $t("nav.bookmarks") }}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
<li v-if="currentUser && currentUser.locked">
|
<li v-if="currentUser && currentUser.locked">
|
||||||
<router-link :to="{ name: 'friend-requests' }">
|
<router-link :to="{ name: 'friend-requests' }">
|
||||||
<i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }}
|
<i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }}
|
||||||
|
|
|
@ -27,6 +27,11 @@ const Notifications = {
|
||||||
seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
|
seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
const store = this.$store
|
||||||
|
const credentials = store.state.users.currentUser.credentials
|
||||||
|
notificationsFetcher.fetchAndUpdate({ store, credentials })
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
mainClass () {
|
mainClass () {
|
||||||
return this.minimalMode ? '' : 'panel panel-default'
|
return this.minimalMode ? '' : 'panel panel-default'
|
||||||
|
@ -56,11 +61,6 @@ const Notifications = {
|
||||||
components: {
|
components: {
|
||||||
Notification
|
Notification
|
||||||
},
|
},
|
||||||
created () {
|
|
||||||
const { dispatch } = this.$store
|
|
||||||
|
|
||||||
dispatch('fetchAndUpdateNotifications')
|
|
||||||
},
|
|
||||||
watch: {
|
watch: {
|
||||||
unseenCount (count) {
|
unseenCount (count) {
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
|
|
|
@ -171,7 +171,7 @@ const PostStatusForm = {
|
||||||
return !!this.preview || this.previewLoading
|
return !!this.preview || this.previewLoading
|
||||||
},
|
},
|
||||||
emptyStatus () {
|
emptyStatus () {
|
||||||
return this.newStatus.status === '' && this.newStatus.files.length === 0
|
return this.newStatus.status.trim() === '' && this.newStatus.files.length === 0
|
||||||
},
|
},
|
||||||
...mapGetters(['mergedConfig'])
|
...mapGetters(['mergedConfig'])
|
||||||
},
|
},
|
||||||
|
@ -182,6 +182,9 @@ const PostStatusForm = {
|
||||||
} else if (this.preview) {
|
} else if (this.preview) {
|
||||||
this.previewStatus(this.newStatus)
|
this.previewStatus(this.newStatus)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'newStatus.spoilerText': function () {
|
||||||
|
this.autoPreview()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -236,7 +239,7 @@ const PostStatusForm = {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
previewStatus () {
|
previewStatus () {
|
||||||
if (this.emptyStatus) {
|
if (this.emptyStatus && this.newStatus.spoilerText.trim() === '') {
|
||||||
this.preview = { error: this.$t('status.preview_empty') }
|
this.preview = { error: this.$t('status.preview_empty') }
|
||||||
this.previewLoading = false
|
this.previewLoading = false
|
||||||
return
|
return
|
||||||
|
@ -269,7 +272,7 @@ const PostStatusForm = {
|
||||||
this.previewLoading = false
|
this.previewLoading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
debouncePreviewStatus: debounce(function () { this.previewStatus() }, 750),
|
debouncePreviewStatus: debounce(function () { this.previewStatus() }, 500),
|
||||||
autoPreview () {
|
autoPreview () {
|
||||||
if (!this.preview) return
|
if (!this.preview) return
|
||||||
this.previewLoading = true
|
this.previewLoading = true
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-body {
|
>.panel-body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,9 @@ const FilteringTab = {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
|
},
|
||||||
|
replyVisibility () {
|
||||||
|
this.$store.dispatch('queueFlushAll')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,14 @@
|
||||||
<i class="button-icon icon-home-2" /> {{ $t("nav.timeline") }}
|
<i class="button-icon icon-home-2" /> {{ $t("nav.timeline") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="currentUser"
|
||||||
|
@click="toggleDrawer"
|
||||||
|
>
|
||||||
|
<router-link :to="{ name: 'bookmarks'}">
|
||||||
|
<i class="button-icon icon-bookmark" /> {{ $t("nav.bookmarks") }}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="currentUser && currentUser.locked"
|
v-if="currentUser && currentUser.locked"
|
||||||
@click="toggleDrawer"
|
@click="toggleDrawer"
|
||||||
|
|
|
@ -141,7 +141,7 @@ const Status = {
|
||||||
return this.mergedConfig.hideFilteredStatuses
|
return this.mergedConfig.hideFilteredStatuses
|
||||||
},
|
},
|
||||||
hideStatus () {
|
hideStatus () {
|
||||||
return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses)
|
return this.deleted || (this.muted && this.hideFilteredStatuses)
|
||||||
},
|
},
|
||||||
isFocused () {
|
isFocused () {
|
||||||
// retweet or root of an expanded conversation
|
// retweet or root of an expanded conversation
|
||||||
|
@ -164,37 +164,6 @@ const Status = {
|
||||||
return user && user.screen_name
|
return user && user.screen_name
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hideReply () {
|
|
||||||
if (this.mergedConfig.replyVisibility === 'all') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (this.inConversation || !this.isReply) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (this.status.user.id === this.currentUser.id) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (this.status.type === 'retweet') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const checkFollowing = this.mergedConfig.replyVisibility === 'following'
|
|
||||||
for (var i = 0; i < this.status.attentions.length; ++i) {
|
|
||||||
if (this.status.user.id === this.status.attentions[i].id) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// There's zero guarantee of this working. If we happen to have that user and their
|
|
||||||
// relationship in store then it will work, but there's kinda little chance of having
|
|
||||||
// them for people you're not following.
|
|
||||||
const relationship = this.$store.state.users.relationships[this.status.attentions[i].id]
|
|
||||||
if (checkFollowing && relationship && relationship.following) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (this.status.attentions[i].id === this.currentUser.id) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.status.attentions.length > 0
|
|
||||||
},
|
|
||||||
replySubject () {
|
replySubject () {
|
||||||
if (!this.status.summary) return ''
|
if (!this.status.summary) return ''
|
||||||
const decodedSummary = unescape(this.status.summary)
|
const decodedSummary = unescape(this.status.summary)
|
||||||
|
|
|
@ -197,7 +197,7 @@
|
||||||
>
|
>
|
||||||
<StatusPopover
|
<StatusPopover
|
||||||
v-if="!isPreview"
|
v-if="!isPreview"
|
||||||
:status-id="status.in_reply_to_status_id"
|
:status-id="status.parent_visible && status.in_reply_to_status_id"
|
||||||
class="reply-to-popover"
|
class="reply-to-popover"
|
||||||
style="min-width: 0"
|
style="min-width: 0"
|
||||||
>
|
>
|
||||||
|
@ -208,7 +208,12 @@
|
||||||
@click.prevent="gotoOriginal(status.in_reply_to_status_id)"
|
@click.prevent="gotoOriginal(status.in_reply_to_status_id)"
|
||||||
>
|
>
|
||||||
<i class="button-icon icon-reply" />
|
<i class="button-icon icon-reply" />
|
||||||
<span class="faint-link reply-to-text">{{ $t('status.reply_to') }}</span>
|
<span
|
||||||
|
class="faint-link reply-to-text"
|
||||||
|
:class="{ 'strikethrough': !status.parent_visible }"
|
||||||
|
>
|
||||||
|
{{ $t('status.reply_to') }}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</StatusPopover>
|
</StatusPopover>
|
||||||
<span
|
<span
|
||||||
|
@ -523,6 +528,10 @@ $status-margin: 0.75em;
|
||||||
margin: 0 0.4em 0 0.2em;
|
margin: 0 0.4em 0 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.strikethrough {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
.replies-separator {
|
.replies-separator {
|
||||||
margin-left: 0.4em;
|
margin-left: 0.4em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,14 +44,14 @@ const StatusContent = {
|
||||||
return lengthScore > 20
|
return lengthScore > 20
|
||||||
},
|
},
|
||||||
longSubject () {
|
longSubject () {
|
||||||
return this.status.summary.length > 900
|
return this.status.summary.length > 240
|
||||||
},
|
},
|
||||||
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
|
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
|
||||||
mightHideBecauseSubject () {
|
mightHideBecauseSubject () {
|
||||||
return this.status.summary && (!this.tallStatus || this.localCollapseSubjectDefault)
|
return !!this.status.summary && this.localCollapseSubjectDefault
|
||||||
},
|
},
|
||||||
mightHideBecauseTall () {
|
mightHideBecauseTall () {
|
||||||
return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault)
|
return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
|
||||||
},
|
},
|
||||||
hideSubjectStatus () {
|
hideSubjectStatus () {
|
||||||
return this.mightHideBecauseSubject && !this.expandingSubject
|
return this.mightHideBecauseSubject && !this.expandingSubject
|
||||||
|
@ -142,12 +142,6 @@ const StatusContent = {
|
||||||
return html
|
return html
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
contentHtml () {
|
|
||||||
if (!this.status.summary_html) {
|
|
||||||
return this.postBodyHtml
|
|
||||||
}
|
|
||||||
return this.status.summary_html + '<br />' + this.postBodyHtml
|
|
||||||
},
|
|
||||||
...mapGetters(['mergedConfig']),
|
...mapGetters(['mergedConfig']),
|
||||||
...mapState({
|
...mapState({
|
||||||
betterShadow: state => state.interface.browserSupport.cssFilter,
|
betterShadow: state => state.interface.browserSupport.cssFilter,
|
||||||
|
|
|
@ -3,18 +3,57 @@
|
||||||
<div class="status-body">
|
<div class="status-body">
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
<div
|
<div
|
||||||
v-if="longSubject"
|
v-if="status.summary_html"
|
||||||
class="status-content-wrapper"
|
class="summary-wrapper"
|
||||||
:class="{ 'tall-status': !showingLongSubject }"
|
:class="{ 'tall-subject': (longSubject && !showingLongSubject) }"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
class="media-body summary"
|
||||||
|
@click.prevent="linkClicked"
|
||||||
|
v-html="status.summary_html"
|
||||||
|
/>
|
||||||
<a
|
<a
|
||||||
v-if="!showingLongSubject"
|
v-if="longSubject && showingLongSubject"
|
||||||
class="tall-status-hider"
|
href="#"
|
||||||
:class="{ 'tall-status-hider_focused': focused }"
|
class="tall-subject-hider"
|
||||||
|
@click.prevent="showingLongSubject=false"
|
||||||
|
>{{ $t("status.hide_full_subject") }}</a>
|
||||||
|
<a
|
||||||
|
v-else-if="longSubject"
|
||||||
|
class="tall-subject-hider"
|
||||||
|
:class="{ 'tall-subject-hider_focused': focused }"
|
||||||
href="#"
|
href="#"
|
||||||
@click.prevent="showingLongSubject=true"
|
@click.prevent="showingLongSubject=true"
|
||||||
|
>
|
||||||
|
{{ $t("status.show_full_subject") }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="{'tall-status': hideTallStatus}"
|
||||||
|
class="status-content-wrapper"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="hideTallStatus"
|
||||||
|
class="tall-status-hider"
|
||||||
|
:class="{ 'tall-status-hider_focused': focused }"
|
||||||
|
href="#"
|
||||||
|
@click.prevent="toggleShowMore"
|
||||||
>
|
>
|
||||||
{{ $t("general.show_more") }}
|
{{ $t("general.show_more") }}
|
||||||
|
</a>
|
||||||
|
<div
|
||||||
|
v-if="!hideSubjectStatus"
|
||||||
|
class="status-content media-body"
|
||||||
|
@click.prevent="linkClicked"
|
||||||
|
v-html="postBodyHtml"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
v-if="hideSubjectStatus"
|
||||||
|
href="#"
|
||||||
|
class="cw-status-hider"
|
||||||
|
@click.prevent="toggleShowMore"
|
||||||
|
>
|
||||||
|
{{ $t("status.show_content") }}
|
||||||
<span
|
<span
|
||||||
v-if="hasImageAttachments"
|
v-if="hasImageAttachments"
|
||||||
class="icon-picture"
|
class="icon-picture"
|
||||||
|
@ -28,54 +67,14 @@
|
||||||
class="icon-link"
|
class="icon-link"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<div
|
|
||||||
class="status-content media-body"
|
|
||||||
@click.prevent="linkClicked"
|
|
||||||
v-html="contentHtml"
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
v-if="showingLongSubject"
|
|
||||||
href="#"
|
|
||||||
class="status-unhider"
|
|
||||||
@click.prevent="showingLongSubject=false"
|
|
||||||
>{{ $t("general.show_less") }}</a>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
:class="{'tall-status': hideTallStatus}"
|
|
||||||
class="status-content-wrapper"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
v-if="hideTallStatus"
|
|
||||||
class="tall-status-hider"
|
|
||||||
:class="{ 'tall-status-hider_focused': focused }"
|
|
||||||
href="#"
|
|
||||||
@click.prevent="toggleShowMore"
|
|
||||||
>{{ $t("general.show_more") }}</a>
|
|
||||||
<div
|
|
||||||
v-if="!hideSubjectStatus"
|
|
||||||
class="status-content media-body"
|
|
||||||
@click.prevent="linkClicked"
|
|
||||||
v-html="contentHtml"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="status-content media-body"
|
|
||||||
@click.prevent="linkClicked"
|
|
||||||
v-html="status.summary_html"
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
v-if="hideSubjectStatus"
|
|
||||||
href="#"
|
|
||||||
class="cw-status-hider"
|
|
||||||
@click.prevent="toggleShowMore"
|
|
||||||
>{{ $t("general.show_more") }}</a>
|
|
||||||
<a
|
<a
|
||||||
v-if="showingMore"
|
v-if="showingMore"
|
||||||
href="#"
|
href="#"
|
||||||
class="status-unhider"
|
class="status-unhider"
|
||||||
@click.prevent="toggleShowMore"
|
@click.prevent="toggleShowMore"
|
||||||
>{{ $t("general.show_less") }}</a>
|
>
|
||||||
|
{{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="status.poll && status.poll.options">
|
<div v-if="status.poll && status.poll.options">
|
||||||
|
@ -129,6 +128,12 @@ $status-margin: 0.75em;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
|
.status-content-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.tall-status {
|
.tall-status {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 220px;
|
height: 220px;
|
||||||
|
@ -136,7 +141,7 @@ $status-margin: 0.75em;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
.status-content {
|
.status-content {
|
||||||
height: 100%;
|
min-height: 0;
|
||||||
mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
|
mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
|
||||||
linear-gradient(to top, white, white);
|
linear-gradient(to top, white, white);
|
||||||
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
/* 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 {
|
.status-content {
|
||||||
font-family: var(--postFont, sans-serif);
|
font-family: var(--postFont, sans-serif);
|
||||||
line-height: 1.4em;
|
line-height: 1.4em;
|
||||||
|
|
|
@ -22,6 +22,10 @@ const StatusPopover = {
|
||||||
methods: {
|
methods: {
|
||||||
enter () {
|
enter () {
|
||||||
if (!this.status) {
|
if (!this.status) {
|
||||||
|
if (!this.statusId) {
|
||||||
|
this.error = true
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$store.dispatch('fetchStatus', this.statusId)
|
this.$store.dispatch('fetchStatus', this.statusId)
|
||||||
.then(data => (this.error = false))
|
.then(data => (this.error = false))
|
||||||
.catch(e => (this.error = true))
|
.catch(e => (this.error = true))
|
||||||
|
|
|
@ -45,11 +45,15 @@ const Timeline = {
|
||||||
newStatusCount () {
|
newStatusCount () {
|
||||||
return this.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) {
|
if (this.timeline.flushMarker !== 0) {
|
||||||
return ''
|
return this.$t('timeline.reload')
|
||||||
} else {
|
} else {
|
||||||
return ` (${this.newStatusCount})`
|
return `${this.$t('timeline.show_new')} (${this.newStatusCount})`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
classes () {
|
classes () {
|
||||||
|
@ -112,8 +116,6 @@ const Timeline = {
|
||||||
if (e.key === '.') this.showNewStatuses()
|
if (e.key === '.') this.showNewStatuses()
|
||||||
},
|
},
|
||||||
showNewStatuses () {
|
showNewStatuses () {
|
||||||
if (this.newStatusCount === 0) return
|
|
||||||
|
|
||||||
if (this.timeline.flushMarker !== 0) {
|
if (this.timeline.flushMarker !== 0) {
|
||||||
this.$store.commit('clearTimeline', { timeline: this.timelineName, excludeUserId: true })
|
this.$store.commit('clearTimeline', { timeline: this.timelineName, excludeUserId: true })
|
||||||
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
|
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
|
||||||
|
@ -135,7 +137,7 @@ const Timeline = {
|
||||||
showImmediately: true,
|
showImmediately: true,
|
||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
tag: this.tag
|
tag: this.tag
|
||||||
}).then(statuses => {
|
}).then(({ statuses }) => {
|
||||||
store.commit('setLoading', { timeline: this.timelineName, value: false })
|
store.commit('setLoading', { timeline: this.timelineName, value: false })
|
||||||
if (statuses && statuses.length === 0) {
|
if (statuses && statuses.length === 0) {
|
||||||
this.bottomedOut = true
|
this.bottomedOut = true
|
||||||
|
|
|
@ -19,14 +19,14 @@
|
||||||
{{ errorData.statusText }}
|
{{ errorData.statusText }}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-if="timeline.newStatusCount > 0 && !timelineError && !errorData"
|
v-else-if="showLoadButton"
|
||||||
class="loadmore-button"
|
class="loadmore-button"
|
||||||
@click.prevent="showNewStatuses"
|
@click.prevent="showNewStatuses"
|
||||||
>
|
>
|
||||||
{{ $t('timeline.show_new') }}{{ newStatusCountStr }}
|
{{ loadButtonString }}
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-if="!timeline.newStatusCount > 0 && !timelineError && !errorData"
|
v-else
|
||||||
class="loadmore-text faint"
|
class="loadmore-text faint"
|
||||||
@click.prevent
|
@click.prevent
|
||||||
>
|
>
|
||||||
|
|
|
@ -120,6 +120,7 @@
|
||||||
"public_tl": "Public Timeline",
|
"public_tl": "Public Timeline",
|
||||||
"timeline": "Timeline",
|
"timeline": "Timeline",
|
||||||
"twkn": "The Whole Known Network",
|
"twkn": "The Whole Known Network",
|
||||||
|
"bookmarks": "Bookmarks",
|
||||||
"user_search": "User Search",
|
"user_search": "User Search",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"who_to_follow": "Who to follow",
|
"who_to_follow": "Who to follow",
|
||||||
|
@ -163,6 +164,9 @@
|
||||||
"load_all_hint": "Loaded first {saneAmount} emoji, loading all emoji may cause performance issues.",
|
"load_all_hint": "Loaded first {saneAmount} emoji, loading all emoji may cause performance issues.",
|
||||||
"load_all": "Loading all {emojiAmount} emoji"
|
"load_all": "Loading all {emojiAmount} emoji"
|
||||||
},
|
},
|
||||||
|
"errors": {
|
||||||
|
"storage_unavailable": "Pleroma could not access browser storage. Your login or your local settings won't be saved and you might encounter unexpected issues. Try enabling cookies."
|
||||||
|
},
|
||||||
"interactions": {
|
"interactions": {
|
||||||
"favs_repeats": "Repeats and Favorites",
|
"favs_repeats": "Repeats and Favorites",
|
||||||
"follows": "New follows",
|
"follows": "New follows",
|
||||||
|
@ -617,6 +621,7 @@
|
||||||
"no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated",
|
"no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated",
|
||||||
"repeated": "repeated",
|
"repeated": "repeated",
|
||||||
"show_new": "Show new",
|
"show_new": "Show new",
|
||||||
|
"reload": "Reload",
|
||||||
"up_to_date": "Up-to-date",
|
"up_to_date": "Up-to-date",
|
||||||
"no_more_statuses": "No more statuses",
|
"no_more_statuses": "No more statuses",
|
||||||
"no_statuses": "No statuses"
|
"no_statuses": "No statuses"
|
||||||
|
@ -628,6 +633,8 @@
|
||||||
"pin": "Pin on profile",
|
"pin": "Pin on profile",
|
||||||
"unpin": "Unpin from profile",
|
"unpin": "Unpin from profile",
|
||||||
"pinned": "Pinned",
|
"pinned": "Pinned",
|
||||||
|
"bookmark": "Bookmark",
|
||||||
|
"unbookmark": "Unbookmark",
|
||||||
"delete_confirm": "Do you really want to delete this status?",
|
"delete_confirm": "Do you really want to delete this status?",
|
||||||
"reply_to": "Reply to",
|
"reply_to": "Reply to",
|
||||||
"replies_list": "Replies:",
|
"replies_list": "Replies:",
|
||||||
|
@ -638,7 +645,11 @@
|
||||||
"thread_muted": "Thread muted",
|
"thread_muted": "Thread muted",
|
||||||
"thread_muted_and_words": ", has words:",
|
"thread_muted_and_words": ", has words:",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"preview_empty": "Empty"
|
"preview_empty": "Empty",
|
||||||
|
"show_full_subject": "Show full subject",
|
||||||
|
"hide_full_subject": "Hide full subject",
|
||||||
|
"show_content": "Show content",
|
||||||
|
"hide_content": "Hide content"
|
||||||
},
|
},
|
||||||
"user_card": {
|
"user_card": {
|
||||||
"approve": "Approve",
|
"approve": "Approve",
|
||||||
|
@ -721,7 +732,8 @@
|
||||||
"add_reaction": "Add Reaction",
|
"add_reaction": "Add Reaction",
|
||||||
"user_settings": "User Settings",
|
"user_settings": "User Settings",
|
||||||
"accept_follow_request": "Accept follow request",
|
"accept_follow_request": "Accept follow request",
|
||||||
"reject_follow_request": "Reject follow request"
|
"reject_follow_request": "Reject follow request",
|
||||||
|
"bookmark": "Bookmark"
|
||||||
},
|
},
|
||||||
"upload": {
|
"upload": {
|
||||||
"error": {
|
"error": {
|
||||||
|
|
|
@ -476,7 +476,14 @@
|
||||||
"backend_version": "Backend Versie",
|
"backend_version": "Backend Versie",
|
||||||
"title": "Versie"
|
"title": "Versie"
|
||||||
},
|
},
|
||||||
"mutes_and_blocks": "Negeringen en Blokkades"
|
"mutes_and_blocks": "Negeringen en Blokkades",
|
||||||
|
"profile_fields": {
|
||||||
|
"value": "Inhoud",
|
||||||
|
"name": "Label",
|
||||||
|
"add_field": "Veld Toevoegen",
|
||||||
|
"label": "Profiel metadata"
|
||||||
|
},
|
||||||
|
"bot": "Dit is een bot account"
|
||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"collapse": "Inklappen",
|
"collapse": "Inklappen",
|
||||||
|
|
|
@ -45,7 +45,8 @@
|
||||||
"timeline": "Лента",
|
"timeline": "Лента",
|
||||||
"twkn": "Федеративная лента",
|
"twkn": "Федеративная лента",
|
||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
"friend_requests": "Запросы на чтение"
|
"friend_requests": "Запросы на чтение",
|
||||||
|
"bookmarks": "Закладки"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"broken_favorite": "Неизвестный статус, ищем...",
|
"broken_favorite": "Неизвестный статус, ищем...",
|
||||||
|
@ -366,6 +367,10 @@
|
||||||
"show_new": "Показать новые",
|
"show_new": "Показать новые",
|
||||||
"up_to_date": "Обновлено"
|
"up_to_date": "Обновлено"
|
||||||
},
|
},
|
||||||
|
"status": {
|
||||||
|
"bookmark": "В закладки",
|
||||||
|
"unbookmark": "Удалить из закладок"
|
||||||
|
},
|
||||||
"user_card": {
|
"user_card": {
|
||||||
"block": "Заблокировать",
|
"block": "Заблокировать",
|
||||||
"blocked": "Заблокирован",
|
"blocked": "Заблокирован",
|
||||||
|
|
16
src/main.js
16
src/main.js
|
@ -62,7 +62,15 @@ const persistedStateOptions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
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({
|
const store = new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
i18n: {
|
i18n: {
|
||||||
|
@ -85,11 +93,13 @@ const persistedStateOptions = {
|
||||||
polls: pollsModule,
|
polls: pollsModule,
|
||||||
postStatus: postStatusModule
|
postStatus: postStatusModule
|
||||||
},
|
},
|
||||||
plugins: [persistedState, pushNotifications],
|
plugins,
|
||||||
strict: false // Socket modifies itself, let's ignore this for now.
|
strict: false // Socket modifies itself, let's ignore this for now.
|
||||||
// strict: process.env.NODE_ENV !== 'production'
|
// strict: process.env.NODE_ENV !== 'production'
|
||||||
})
|
})
|
||||||
|
if (storageError) {
|
||||||
|
store.dispatch('pushGlobalNotice', { messageKey: 'errors.storage_unavailable', level: 'error' })
|
||||||
|
}
|
||||||
afterStoreSetup({ store, i18n })
|
afterStoreSetup({ store, i18n })
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
|
|
@ -138,9 +138,6 @@ const api = {
|
||||||
if (!fetcher) return
|
if (!fetcher) return
|
||||||
store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
|
store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
|
||||||
},
|
},
|
||||||
fetchAndUpdateNotifications (store) {
|
|
||||||
store.state.backendInteractor.fetchAndUpdateNotifications({ store })
|
|
||||||
},
|
|
||||||
|
|
||||||
// Follow requests
|
// Follow requests
|
||||||
startFetchingFollowRequests (store) {
|
startFetchingFollowRequests (store) {
|
||||||
|
|
|
@ -14,7 +14,8 @@ const defaultState = {
|
||||||
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
|
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
mobileLayout: false
|
mobileLayout: false,
|
||||||
|
globalNotices: []
|
||||||
}
|
}
|
||||||
|
|
||||||
const interfaceMod = {
|
const interfaceMod = {
|
||||||
|
@ -58,6 +59,12 @@ const interfaceMod = {
|
||||||
if (!state.settingsModalLoaded) {
|
if (!state.settingsModalLoaded) {
|
||||||
state.settingsModalLoaded = true
|
state.settingsModalLoaded = true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
pushGlobalNotice (state, notice) {
|
||||||
|
state.globalNotices.push(notice)
|
||||||
|
},
|
||||||
|
removeGlobalNotice (state, notice) {
|
||||||
|
state.globalNotices = state.globalNotices.filter(n => n !== notice)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -81,6 +88,28 @@ const interfaceMod = {
|
||||||
},
|
},
|
||||||
togglePeekSettingsModal ({ commit }) {
|
togglePeekSettingsModal ({ commit }) {
|
||||||
commit('togglePeekSettingsModal')
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,8 @@ export const defaultState = () => ({
|
||||||
publicAndExternal: emptyTl(),
|
publicAndExternal: emptyTl(),
|
||||||
friends: emptyTl(),
|
friends: emptyTl(),
|
||||||
tag: 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 = {},
|
const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId, pagination = {} }) => {
|
||||||
noIdUpdate = false, userId }) => {
|
|
||||||
// Sanity check
|
// Sanity check
|
||||||
if (!isArray(statuses)) {
|
if (!isArray(statuses)) {
|
||||||
return false
|
return false
|
||||||
|
@ -173,8 +173,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
||||||
const allStatuses = state.allStatuses
|
const allStatuses = state.allStatuses
|
||||||
const timelineObject = state.timelines[timeline]
|
const timelineObject = state.timelines[timeline]
|
||||||
|
|
||||||
const maxNew = statuses.length > 0 ? maxBy(statuses, 'id').id : 0
|
// Mismatch between API pagination and our internal minId/maxId tracking systems:
|
||||||
const minNew = statuses.length > 0 ? minBy(statuses, 'id').id : 0
|
// 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 newer = timeline && (maxNew > timelineObject.maxId || timelineObject.maxId === 0) && statuses.length > 0
|
||||||
const older = timeline && (minNew < timelineObject.minId || timelineObject.minId === 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
|
// Keep the visible statuses sorted
|
||||||
if (timeline) {
|
if (timeline && !(timeline === 'bookmarks')) {
|
||||||
sortTimeline(timelineObject)
|
sortTimeline(timelineObject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -463,6 +468,14 @@ export const mutations = {
|
||||||
newStatus.rebloggedBy.push(user)
|
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 }) {
|
setDeleted (state, { status }) {
|
||||||
const newStatus = state.allStatusesObject[status.id]
|
const newStatus = state.allStatusesObject[status.id]
|
||||||
newStatus.deleted = true
|
newStatus.deleted = true
|
||||||
|
@ -515,6 +528,11 @@ export const mutations = {
|
||||||
queueFlush (state, { timeline, id }) {
|
queueFlush (state, { timeline, id }) {
|
||||||
state.timelines[timeline].flushMarker = 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 }) {
|
addRepeats (state, { id, rebloggedByUsers, currentUser }) {
|
||||||
const newStatus = state.allStatusesObject[id]
|
const newStatus = state.allStatusesObject[id]
|
||||||
newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _)
|
newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _)
|
||||||
|
@ -585,8 +603,8 @@ export const mutations = {
|
||||||
const statuses = {
|
const statuses = {
|
||||||
state: defaultState(),
|
state: defaultState(),
|
||||||
actions: {
|
actions: {
|
||||||
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId }) {
|
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) {
|
||||||
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId })
|
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination })
|
||||||
},
|
},
|
||||||
addNewNotifications ({ rootState, commit, dispatch, rootGetters }, { notifications, older }) {
|
addNewNotifications ({ rootState, commit, dispatch, rootGetters }, { notifications, older }) {
|
||||||
commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older, rootGetters })
|
commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older, rootGetters })
|
||||||
|
@ -661,9 +679,26 @@ const statuses = {
|
||||||
rootState.api.backendInteractor.unretweet({ id: status.id })
|
rootState.api.backendInteractor.unretweet({ id: status.id })
|
||||||
.then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser }))
|
.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 }) {
|
queueFlush ({ rootState, commit }, { timeline, id }) {
|
||||||
commit('queueFlush', { timeline, id })
|
commit('queueFlush', { timeline, id })
|
||||||
},
|
},
|
||||||
|
queueFlushAll ({ rootState, commit }) {
|
||||||
|
commit('queueFlushAll')
|
||||||
|
},
|
||||||
markNotificationsAsSeen ({ rootState, commit }) {
|
markNotificationsAsSeen ({ rootState, commit }) {
|
||||||
commit('markNotificationsAsSeen')
|
commit('markNotificationsAsSeen')
|
||||||
apiService.markNotificationsAsSeen({
|
apiService.markNotificationsAsSeen({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { each, map, concat, last, get } from 'lodash'
|
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'
|
import { RegistrationError, StatusCodeError } from '../errors/errors'
|
||||||
|
|
||||||
/* eslint-env browser */
|
/* 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_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
|
||||||
const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
|
const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
|
||||||
const MASTODON_TAG_TIMELINE_URL = tag => `/api/v1/timelines/tag/${tag}`
|
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_BLOCKS_URL = '/api/v1/blocks/'
|
||||||
const MASTODON_USER_MUTES_URL = '/api/v1/mutes/'
|
const MASTODON_USER_MUTES_URL = '/api/v1/mutes/'
|
||||||
const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block`
|
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_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
|
||||||
const MASTODON_SUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/subscribe`
|
const MASTODON_SUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/subscribe`
|
||||||
const MASTODON_UNSUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/unsubscribe`
|
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_POST_STATUS_URL = '/api/v1/statuses'
|
||||||
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
|
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
|
||||||
const MASTODON_VOTE_URL = id => `/api/v1/polls/${id}/votes`
|
const MASTODON_VOTE_URL = id => `/api/v1/polls/${id}/votes`
|
||||||
|
@ -498,7 +501,8 @@ const fetchTimeline = ({
|
||||||
until = false,
|
until = false,
|
||||||
userId = false,
|
userId = false,
|
||||||
tag = false,
|
tag = false,
|
||||||
withMuted = false
|
withMuted = false,
|
||||||
|
replyVisibility = 'all'
|
||||||
}) => {
|
}) => {
|
||||||
const timelineUrls = {
|
const timelineUrls = {
|
||||||
public: MASTODON_PUBLIC_TIMELINE,
|
public: MASTODON_PUBLIC_TIMELINE,
|
||||||
|
@ -509,7 +513,8 @@ const fetchTimeline = ({
|
||||||
user: MASTODON_USER_TIMELINE_URL,
|
user: MASTODON_USER_TIMELINE_URL,
|
||||||
media: MASTODON_USER_TIMELINE_URL,
|
media: MASTODON_USER_TIMELINE_URL,
|
||||||
favorites: MASTODON_USER_FAVORITES_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 isNotifications = timeline === 'notifications'
|
||||||
const params = []
|
const params = []
|
||||||
|
@ -538,9 +543,12 @@ const fetchTimeline = ({
|
||||||
if (timeline === 'public' || timeline === 'publicAndExternal') {
|
if (timeline === 'public' || timeline === 'publicAndExternal') {
|
||||||
params.push(['only_media', false])
|
params.push(['only_media', false])
|
||||||
}
|
}
|
||||||
if (timeline !== 'favorites') {
|
if (timeline !== 'favorites' && timeline !== 'bookmarks') {
|
||||||
params.push(['with_muted', withMuted])
|
params.push(['with_muted', withMuted])
|
||||||
}
|
}
|
||||||
|
if (replyVisibility !== 'all') {
|
||||||
|
params.push(['reply_visibility', replyVisibility])
|
||||||
|
}
|
||||||
|
|
||||||
params.push(['limit', 20])
|
params.push(['limit', 20])
|
||||||
|
|
||||||
|
@ -548,16 +556,20 @@ const fetchTimeline = ({
|
||||||
url += `?${queryString}`
|
url += `?${queryString}`
|
||||||
let status = ''
|
let status = ''
|
||||||
let statusText = ''
|
let statusText = ''
|
||||||
|
let pagination = {}
|
||||||
return fetch(url, { headers: authHeaders(credentials) })
|
return fetch(url, { headers: authHeaders(credentials) })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
status = data.status
|
status = data.status
|
||||||
statusText = data.statusText
|
statusText = data.statusText
|
||||||
|
pagination = parseLinkHeaderPagination(data.headers.get('Link'), {
|
||||||
|
flakeId: timeline !== 'bookmarks' && timeline !== 'notifications'
|
||||||
|
})
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (!data.error) {
|
if (!data.error) {
|
||||||
return data.map(isNotifications ? parseNotification : parseStatus)
|
return { data: data.map(isNotifications ? parseNotification : parseStatus), pagination }
|
||||||
} else {
|
} else {
|
||||||
data.status = status
|
data.status = status
|
||||||
data.statusText = statusText
|
data.statusText = statusText
|
||||||
|
@ -608,6 +620,22 @@ const unretweet = ({ id, credentials }) => {
|
||||||
.then((data) => parseStatus(data))
|
.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 = ({
|
const postStatus = ({
|
||||||
credentials,
|
credentials,
|
||||||
status,
|
status,
|
||||||
|
@ -1144,6 +1172,8 @@ const apiService = {
|
||||||
unfavorite,
|
unfavorite,
|
||||||
retweet,
|
retweet,
|
||||||
unretweet,
|
unretweet,
|
||||||
|
bookmarkStatus,
|
||||||
|
unbookmarkStatus,
|
||||||
postStatus,
|
postStatus,
|
||||||
deleteStatus,
|
deleteStatus,
|
||||||
uploadMedia,
|
uploadMedia,
|
||||||
|
|
|
@ -12,10 +12,6 @@ const backendInteractorService = credentials => ({
|
||||||
return notificationsFetcher.startFetching({ store, credentials })
|
return notificationsFetcher.startFetching({ store, credentials })
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchAndUpdateNotifications ({ store }) {
|
|
||||||
return notificationsFetcher.fetchAndUpdate({ store, credentials })
|
|
||||||
},
|
|
||||||
|
|
||||||
startFetchingFollowRequests ({ store }) {
|
startFetchingFollowRequests ({ store }) {
|
||||||
return followRequestFetcher.startFetching({ store, credentials })
|
return followRequestFetcher.startFetching({ store, credentials })
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import escape from 'escape-html'
|
import escape from 'escape-html'
|
||||||
|
import parseLinkHeader from 'parse-link-header'
|
||||||
import { isStatusNotification } from '../notification_utils/notification_utils.js'
|
import { isStatusNotification } from '../notification_utils/notification_utils.js'
|
||||||
|
|
||||||
const qvitterStatusType = (status) => {
|
const qvitterStatusType = (status) => {
|
||||||
|
@ -232,6 +233,8 @@ export const parseStatus = (data) => {
|
||||||
output.repeated = data.reblogged
|
output.repeated = data.reblogged
|
||||||
output.repeat_num = data.reblogs_count
|
output.repeat_num = data.reblogs_count
|
||||||
|
|
||||||
|
output.bookmarked = data.bookmarked
|
||||||
|
|
||||||
output.type = data.reblog ? 'retweet' : 'status'
|
output.type = data.reblog ? 'retweet' : 'status'
|
||||||
output.nsfw = data.sensitive
|
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.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct
|
||||||
output.thread_muted = pleroma.thread_muted
|
output.thread_muted = pleroma.thread_muted
|
||||||
output.emoji_reactions = pleroma.emoji_reactions
|
output.emoji_reactions = pleroma.emoji_reactions
|
||||||
|
output.parent_visible = pleroma.parent_visible === undefined ? true : pleroma.parent_visible
|
||||||
} else {
|
} else {
|
||||||
output.text = data.content
|
output.text = data.content
|
||||||
output.summary = data.spoiler_text
|
output.summary = data.spoiler_text
|
||||||
|
@ -381,3 +385,16 @@ const isNsfw = (status) => {
|
||||||
const nsfwRegex = /#nsfw/i
|
const nsfwRegex = /#nsfw/i
|
||||||
return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ const fetchAndUpdate = ({ store, credentials }) => {
|
||||||
return apiService.fetchFollowRequests({ credentials })
|
return apiService.fetchFollowRequests({ credentials })
|
||||||
.then((requests) => {
|
.then((requests) => {
|
||||||
store.commit('setFollowRequests', requests)
|
store.commit('setFollowRequests', requests)
|
||||||
|
store.commit('addNewUsers', requests)
|
||||||
}, () => {})
|
}, () => {})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,21 +27,25 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
|
||||||
}
|
}
|
||||||
const result = fetchNotifications({ store, args, older })
|
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 notifications = timelineData.data
|
||||||
const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id)
|
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)
|
args['since'] = Math.max(...readNotifsIds)
|
||||||
fetchNotifications({ store, args, older })
|
fetchNotifications({ store, args, older })
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchNotifications = ({ store, args, older }) => {
|
const fetchNotifications = ({ store, args, older }) => {
|
||||||
return apiService.fetchTimeline(args)
|
return apiService.fetchTimeline(args)
|
||||||
.then((notifications) => {
|
.then(({ data: notifications }) => {
|
||||||
update({ store, notifications, older })
|
update({ store, notifications, older })
|
||||||
return notifications
|
return notifications
|
||||||
}, () => store.dispatch('setNotificationsError', { value: true }))
|
}, () => store.dispatch('setNotificationsError', { value: true }))
|
||||||
|
|
|
@ -34,7 +34,8 @@ export const DEFAULT_OPACITY = {
|
||||||
alert: 0.5,
|
alert: 0.5,
|
||||||
input: 0.5,
|
input: 0.5,
|
||||||
faint: 0.5,
|
faint: 0.5,
|
||||||
underlay: 0.15
|
underlay: 0.15,
|
||||||
|
alertPopup: 0.95
|
||||||
}
|
}
|
||||||
|
|
||||||
/** SUBJECT TO CHANGE IN THE FUTURE, this is all beta
|
/** SUBJECT TO CHANGE IN THE FUTURE, this is all beta
|
||||||
|
@ -627,6 +628,39 @@ export const SLOT_INHERITANCE = {
|
||||||
textColor: true
|
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',
|
badgeNotification: '--cRed',
|
||||||
badgeNotificationText: {
|
badgeNotificationText: {
|
||||||
depends: ['text', 'badgeNotification'],
|
depends: ['text', 'badgeNotification'],
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { camelCase } from 'lodash'
|
||||||
|
|
||||||
import apiService from '../api/api.service.js'
|
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)
|
const ccTimeline = camelCase(timeline)
|
||||||
|
|
||||||
store.dispatch('setError', { value: false })
|
store.dispatch('setError', { value: false })
|
||||||
|
@ -12,7 +12,8 @@ const update = ({ store, statuses, timeline, showImmediately, userId }) => {
|
||||||
timeline: ccTimeline,
|
timeline: ccTimeline,
|
||||||
userId,
|
userId,
|
||||||
statuses,
|
statuses,
|
||||||
showImmediately
|
showImmediately,
|
||||||
|
pagination
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +31,8 @@ const fetchAndUpdate = ({
|
||||||
const rootState = store.rootState || store.state
|
const rootState = store.rootState || store.state
|
||||||
const { getters } = store
|
const { getters } = store
|
||||||
const timelineData = rootState.statuses.timelines[camelCase(timeline)]
|
const timelineData = rootState.statuses.timelines[camelCase(timeline)]
|
||||||
const hideMutedPosts = getters.mergedConfig.hideMutedPosts
|
const { hideMutedPosts, replyVisibility } = getters.mergedConfig
|
||||||
|
const loggedIn = !!rootState.users.currentUser
|
||||||
|
|
||||||
if (older) {
|
if (older) {
|
||||||
args['until'] = until || timelineData.minId
|
args['until'] = until || timelineData.minId
|
||||||
|
@ -41,20 +43,23 @@ const fetchAndUpdate = ({
|
||||||
args['userId'] = userId
|
args['userId'] = userId
|
||||||
args['tag'] = tag
|
args['tag'] = tag
|
||||||
args['withMuted'] = !hideMutedPosts
|
args['withMuted'] = !hideMutedPosts
|
||||||
|
if (loggedIn) args['replyVisibility'] = replyVisibility
|
||||||
|
|
||||||
const numStatusesBeforeFetch = timelineData.statuses.length
|
const numStatusesBeforeFetch = timelineData.statuses.length
|
||||||
|
|
||||||
return apiService.fetchTimeline(args)
|
return apiService.fetchTimeline(args)
|
||||||
.then((statuses) => {
|
.then(response => {
|
||||||
if (statuses.error) {
|
if (response.error) {
|
||||||
store.dispatch('setErrorData', { value: statuses })
|
store.dispatch('setErrorData', { value: response })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { data: statuses, pagination } = response
|
||||||
if (!older && statuses.length >= 20 && !timelineData.loading && numStatusesBeforeFetch > 0) {
|
if (!older && statuses.length >= 20 && !timelineData.loading && numStatusesBeforeFetch > 0) {
|
||||||
store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId })
|
store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId })
|
||||||
}
|
}
|
||||||
update({ store, statuses, timeline, showImmediately, userId })
|
update({ store, statuses, timeline, showImmediately, userId, pagination })
|
||||||
return statuses
|
return { statuses, pagination }
|
||||||
}, () => store.dispatch('setError', { value: true }))
|
}, () => store.dispatch('setError', { value: true }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
12
static/fontello.json
Executable file → Normal file
12
static/fontello.json
Executable file → Normal file
|
@ -375,6 +375,18 @@
|
||||||
"css": "download",
|
"css": "download",
|
||||||
"code": 59429,
|
"code": 59429,
|
||||||
"src": "fontawesome"
|
"src": "fontawesome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "f04a5d24e9e659145b966739c4fde82a",
|
||||||
|
"css": "bookmark",
|
||||||
|
"code": 59430,
|
||||||
|
"src": "fontawesome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "2f5ef6f6b7aaebc56458ab4e865beff5",
|
||||||
|
"css": "bookmark-empty",
|
||||||
|
"code": 61591,
|
||||||
|
"src": "fontawesome"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -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 mastoapidata from '../../../../fixtures/mastoapi.json'
|
||||||
import qvitterapidata from '../../../../fixtures/statuses.json'
|
import qvitterapidata from '../../../../fixtures/statuses.json'
|
||||||
|
|
||||||
|
@ -383,4 +383,24 @@ describe('API Entities normalizer', () => {
|
||||||
expect(result).to.include('title=\':[a-z] {|}*:\'')
|
expect(result).to.include('title=\':[a-z] {|}*:\'')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Link header pagination', () => {
|
||||||
|
it('Parses min and max ids as integers', () => {
|
||||||
|
const linkHeader = '<https://example.com/api/v1/notifications?max_id=861676>; rel="next", <https://example.com/api/v1/notifications?min_id=861741>; rel="prev"'
|
||||||
|
const result = parseLinkHeaderPagination(linkHeader)
|
||||||
|
expect(result).to.eql({
|
||||||
|
'maxId': 861676,
|
||||||
|
'minId': 861741
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Parses min and max ids as flakes', () => {
|
||||||
|
const linkHeader = '<http://example.com/api/v1/timelines/home?max_id=9waQx5IIS48qVue2Ai>; rel="next", <http://example.com/api/v1/timelines/home?min_id=9wi61nIPnfn674xgie>; rel="prev"'
|
||||||
|
const result = parseLinkHeaderPagination(linkHeader, { flakeId: true })
|
||||||
|
expect(result).to.eql({
|
||||||
|
'maxId': '9waQx5IIS48qVue2Ai',
|
||||||
|
'minId': '9wi61nIPnfn674xgie'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -5751,6 +5751,13 @@ parse-json@^4.0.0:
|
||||||
error-ex "^1.3.1"
|
error-ex "^1.3.1"
|
||||||
json-parse-better-errors "^1.0.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:
|
parseqs@0.0.5:
|
||||||
version "0.0.5"
|
version "0.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
|
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
|
||||||
|
|
Loading…
Reference in a new issue