diff options
Diffstat (limited to 'src')
40 files changed, 320 insertions, 98 deletions
@@ -90,6 +90,7 @@ export default { }, sitename () { return this.$store.state.instance.name }, chat () { return this.$store.state.chat.channel.state === 'joined' }, + hideSitename () { return this.$store.state.instance.hideSitename }, suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, showInstanceSpecificPanel () { return this.$store.state.instance.showInstanceSpecificPanel && diff --git a/src/App.scss b/src/App.scss index 310962b8..754ca62e 100644 --- a/src/App.scss +++ b/src/App.scss @@ -855,3 +855,31 @@ nav { .btn.btn-default { min-height: 28px; } + +.animate-spin { + animation: spin 2s infinite linear; + display: inline-block; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(359deg); + } +} + +.new-status-notification { + position:relative; + margin-top: -1px; + font-size: 1.1em; + border-width: 1px 0 0 0; + border-style: solid; + border-color: var(--border, $fallback--border); + padding: 10px; + z-index: 1; + background-color: $fallback--fg; + background-color: var(--panel, $fallback--fg); +} diff --git a/src/App.vue b/src/App.vue index 1f244b56..d455e9ed 100644 --- a/src/App.vue +++ b/src/App.vue @@ -31,6 +31,7 @@ </div> <div class="item"> <router-link + v-if="!hideSitename" class="site-name" :to="{ name: 'root' }" active-class="home" diff --git a/src/boot/after_store.js b/src/boot/after_store.js index cbe0c330..f169d7ba 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -108,6 +108,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => { copyInstanceOption('alwaysShowSubjectInput') copyInstanceOption('noAttachmentLinks') copyInstanceOption('showFeaturesPanel') + copyInstanceOption('hideSitename') return store.dispatch('setTheme', config['theme']) } diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js index 204d506a..d2153680 100644 --- a/src/components/account_actions/account_actions.js +++ b/src/components/account_actions/account_actions.js @@ -25,9 +25,6 @@ const AccountActions = { }, reportUser () { this.$store.dispatch('openUserReportingModal', this.user.id) - }, - mentionUser () { - this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user }) } } } diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index 046cba93..d3235be1 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -9,17 +9,7 @@ > <div slot="popover"> <div class="dropdown-menu"> - <button - class="btn btn-default btn-block dropdown-item" - @click="mentionUser" - > - {{ $t('user_card.mention') }} - </button> <template v-if="user.following"> - <div - role="separator" - class="dropdown-divider" - /> <button v-if="user.showing_reblogs" class="btn btn-default dropdown-item" @@ -34,11 +24,11 @@ > {{ $t('user_card.show_repeats') }} </button> + <div + role="separator" + class="dropdown-divider" + /> </template> - <div - role="separator" - class="dropdown-divider" - /> <button v-if="user.statusnet_blocking" class="btn btn-default btn-block dropdown-item" diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 72ee9c39..08283fff 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -43,7 +43,8 @@ const conversation = { 'collapsable', 'isPage', 'pinnedStatusIdsObject', - 'inProfile' + 'inProfile', + 'profileUserId' ], created () { if (this.isPage) { diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 0f1de55f..2e48240a 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -27,6 +27,7 @@ :highlight="getHighlight()" :replies="getReplies(status.id)" :in-profile="inProfile" + :profile-user-id="profileUserId" class="status-fadein panel-body" @goto="setHighlight" @toggleExpanded="toggleExpanded" diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue index 1ca22001..f5ace0cc 100644 --- a/src/components/interface_language_switcher/interface_language_switcher.vue +++ b/src/components/interface_language_switcher/interface_language_switcher.vue @@ -51,8 +51,8 @@ export default { methods: { getLanguageName (code) { const specialLanguageNames = { - 'ja': 'Japanese (やさしいにほんご)', - 'ja_pedantic': 'Japanese (日本語)', + 'ja': 'Japanese (日本語)', + 'ja_easy': 'Japanese (やさしいにほんご)', 'zh': 'Chinese (简体中文)' } return specialLanguageNames[code] || ISO6391.getName(code) diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 49e3143e..80d2a8b9 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -10,13 +10,13 @@ :src="currentMedia.url" @touchstart.stop="mediaTouchStart" @touchmove.stop="mediaTouchMove" + @click="hide" > <VideoAttachment v-if="type === 'video'" class="modal-image" :attachment="currentMedia" :controls="true" - @click.stop.native="" /> <button v-if="canNavigate" diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js index 5a90c31f..c1166a0c 100644 --- a/src/components/mobile_nav/mobile_nav.js +++ b/src/components/mobile_nav/mobile_nav.js @@ -29,6 +29,7 @@ const MobileNav = { unseenNotificationsCount () { return this.unseenNotifications.length }, + hideSitename () { return this.$store.state.instance.hideSitename }, sitename () { return this.$store.state.instance.name } }, methods: { diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue index d1c24e56..51f1d636 100644 --- a/src/components/mobile_nav/mobile_nav.vue +++ b/src/components/mobile_nav/mobile_nav.vue @@ -17,6 +17,7 @@ <i class="button-icon icon-menu" /> </a> <router-link + v-if="!hideSitename" class="site-name" :to="{ name: 'root' }" active-class="home" diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.js b/src/components/mrf_transparency_panel/mrf_transparency_panel.js index 20f8a08a..6a1baec8 100644 --- a/src/components/mrf_transparency_panel/mrf_transparency_panel.js +++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.js @@ -1,16 +1,27 @@ import { mapState } from 'vuex' +import { get } from 'lodash' const MRFTransparencyPanel = { - computed: mapState({ - federationPolicy: state => state.instance.federationPolicy, - mrfPolicies: state => state.instance.federationPolicy.mrf_policies, - acceptInstances: state => state.instance.federationPolicy.mrf_simple.accept, - rejectInstances: state => state.instance.federationPolicy.mrf_simple.reject, - quarantineInstances: state => state.instance.federationPolicy.quarantined_instances, - ftlRemovalInstances: state => state.instance.federationPolicy.mrf_simple.federated_timeline_removal, - mediaNsfwInstances: state => state.instance.federationPolicy.mrf_simple.media_nsfw, - mediaRemovalInstances: state => state.instance.federationPolicy.mrf_simple.media_removal - }) + computed: { + ...mapState({ + federationPolicy: state => get(state, 'instance.federationPolicy'), + mrfPolicies: state => get(state, 'instance.federationPolicy.mrf_policies', []), + quarantineInstances: state => get(state, 'instance.federationPolicy.quarantined_instances', []), + acceptInstances: state => get(state, 'instance.federationPolicy.mrf_simple.accept', []), + rejectInstances: state => get(state, 'instance.federationPolicy.mrf_simple.reject', []), + ftlRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []), + mediaNsfwInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []), + mediaRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_removal', []) + }), + hasInstanceSpecificPolicies () { + return this.quarantineInstances.length || + this.acceptInstances.length || + this.rejectInstances.length || + this.ftlRemovalInstances.length || + this.mediaNsfwInstances.length || + this.mediaRemovalInstances.length + } + } } export default MRFTransparencyPanel diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue index 2640d68c..d6495dc6 100644 --- a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue +++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue @@ -22,7 +22,9 @@ /> </ul> - <h2>{{ $t("about.mrf_policy_simple") }}</h2> + <h2 v-if="hasInstanceSpecificPolicies"> + {{ $t("about.mrf_policy_simple") }} + </h2> <div v-if="acceptInstances.length"> <h4>{{ $t("about.mrf_policy_simple_accept") }}</h4> diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index a6426d13..dbbfb096 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -4,10 +4,7 @@ import { mapState } from 'vuex' const NavPanel = { created () { if (this.currentUser && this.currentUser.locked) { - const store = this.$store - const credentials = store.state.users.currentUser.credentials - - followRequestFetcher.startFetching({ store, credentials }) + this.$store.dispatch('startFetchingFollowRequest') } }, computed: mapState({ diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index d85c28bd..97c36711 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -4,22 +4,22 @@ <ul> <li v-if="currentUser"> <router-link :to="{ name: 'friends' }"> - {{ $t("nav.timeline") }} + <i class="button-icon icon-home-2" /> {{ $t("nav.timeline") }} </router-link> </li> <li v-if="currentUser"> <router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> - {{ $t("nav.interactions") }} + <i class="button-icon icon-bell-alt" /> {{ $t("nav.interactions") }} </router-link> </li> <li v-if="currentUser"> <router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }"> - {{ $t("nav.dms") }} + <i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }} </router-link> </li> <li v-if="currentUser && currentUser.locked"> <router-link :to="{ name: 'friend-requests' }"> - {{ $t("nav.friend_requests") }} + <i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }} <span v-if="followRequestCount > 0" class="badge follow-request-count" @@ -30,17 +30,17 @@ </li> <li v-if="currentUser || !privateMode"> <router-link :to="{ name: 'public-timeline' }"> - {{ $t("nav.public_tl") }} + <i class="button-icon icon-users" /> {{ $t("nav.public_tl") }} </router-link> </li> <li v-if="(currentUser || !privateMode) && federating"> <router-link :to="{ name: 'public-external-timeline' }"> - {{ $t("nav.twkn") }} + <i class="button-icon icon-globe" /> {{ $t("nav.twkn") }} </router-link> </li> <li> <router-link :to="{ name: 'about' }"> - {{ $t("nav.about") }} + <i class="button-icon icon-info-circled" /> {{ $t("nav.about") }} </router-link> </li> </ul> @@ -113,4 +113,8 @@ } } } + +.nav-panel .button-icon:before { + width: 1.1em; +} </style> diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index af6299e4..74067fef 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -169,9 +169,7 @@ const PostStatusForm = { if (this.submitDisabled) { return } if (this.newStatus.status === '') { - if (this.newStatus.files.length > 0) { - this.newStatus.status = '\u200b' // hack - } else { + if (this.newStatus.files.length === 0) { this.error = 'Cannot post an empty status with no files' return } diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue index 5bb06a4f..222b67a8 100644 --- a/src/components/registration/registration.vue +++ b/src/components/registration/registration.vue @@ -172,7 +172,7 @@ for="captcha-label" >{{ $t('captcha') }}</label> - <template v-if="captcha.type == 'kocaptcha'"> + <template v-if="['kocaptcha', 'native'].includes(captcha.type)"> <img :src="captcha.url" @click="setCaptcha" diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index a83489d2..c4021137 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -270,6 +270,17 @@ </li> </ul> </div> + + <div class="setting-item"> + <h2>{{ $t('settings.fun') }}</h2> + <ul class="setting-list"> + <li> + <Checkbox v-model="greentext"> + {{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }} + </Checkbox> + </li> + </ul> + </div> </div> <div :label="$t('settings.theme')"> diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index 2725d43a..22cb1a55 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -10,6 +10,10 @@ const SideDrawer = { }), created () { this.closeGesture = GestureService.swipeGesture(GestureService.DIRECTION_LEFT, this.toggleDrawer) + + if (this.currentUser && this.currentUser.locked) { + this.$store.dispatch('startFetchingFollowRequest') + } }, components: { UserCard }, computed: { @@ -29,6 +33,9 @@ const SideDrawer = { logo () { return this.$store.state.instance.logo }, + hideSitename () { + return this.$store.state.instance.hideSitename + }, sitename () { return this.$store.state.instance.name }, diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index be18a5d7..1454e0d8 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -27,7 +27,7 @@ class="side-drawer-logo-wrapper" > <img :src="logo"> - <span>{{ sitename }}</span> + <span v-if="!hideSitename">{{ sitename }}</span> </div> </div> <ul> @@ -36,7 +36,7 @@ @click="toggleDrawer" > <router-link :to="{ name: 'login' }"> - {{ $t("login.login") }} + <i class="button-icon icon-login" /> {{ $t("login.login") }} </router-link> </li> <li @@ -44,7 +44,7 @@ @click="toggleDrawer" > <router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }"> - {{ $t("nav.dms") }} + <i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }} </router-link> </li> <li @@ -52,7 +52,7 @@ @click="toggleDrawer" > <router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> - {{ $t("nav.interactions") }} + <i class="button-icon icon-bell-alt" /> {{ $t("nav.interactions") }} </router-link> </li> </ul> @@ -62,7 +62,7 @@ @click="toggleDrawer" > <router-link :to="{ name: 'friends' }"> - {{ $t("nav.timeline") }} + <i class="button-icon icon-home-2" /> {{ $t("nav.timeline") }} </router-link> </li> <li @@ -70,7 +70,7 @@ @click="toggleDrawer" > <router-link to="/friend-requests"> - {{ $t("nav.friend_requests") }} + <i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }} <span v-if="followRequestCount > 0" class="badge follow-request-count" @@ -81,12 +81,12 @@ </li> <li @click="toggleDrawer" v-if="currentUser || !privateMode"> <router-link to="/main/public"> - {{ $t("nav.public_tl") }} + <i class="button-icon icon-users" /> {{ $t("nav.public_tl") }} </router-link> </li> <li @click="toggleDrawer" v-if="(currentUser || !privateMode) && federating"> <router-link to="/main/all"> - {{ $t("nav.twkn") }} + <i class="button-icon icon-globe" /> {{ $t("nav.twkn") }} </router-link> </li> <li @@ -94,14 +94,14 @@ @click="toggleDrawer" > <router-link :to="{ name: 'chat' }"> - {{ $t("nav.chat") }} + <i class="button-icon icon-chat" /> {{ $t("nav.chat") }} </router-link> </li> </ul> <ul> <li @click="toggleDrawer" v-if="currentUser || !privateMode"> <router-link :to="{ name: 'search' }"> - {{ $t("nav.search") }} + <i class="button-icon icon-search" /> {{ $t("nav.search") }} </router-link> </li> <li @@ -109,17 +109,17 @@ @click="toggleDrawer" > <router-link :to="{ name: 'who-to-follow' }"> - {{ $t("nav.who_to_follow") }} + <i class="button-icon icon-user-plus" /> {{ $t("nav.who_to_follow") }} </router-link> </li> <li @click="toggleDrawer"> <router-link :to="{ name: 'settings' }"> - {{ $t("settings.settings") }} + <i class="button-icon icon-cog" /> {{ $t("settings.settings") }} </router-link> </li> <li @click="toggleDrawer"> <router-link :to="{ name: 'about'}"> - {{ $t("nav.about") }} + <i class="button-icon icon-info-circled" /> {{ $t("nav.about") }} </router-link> </li> <li @@ -130,7 +130,7 @@ href="/pleroma/admin/#/login-pleroma" target="_blank" > - {{ $t("nav.administration") }} + <i class="button-icon icon-gauge" /> {{ $t("nav.administration") }} </a> </li> <li @@ -141,7 +141,7 @@ href="#" @click="doLogout" > - {{ $t("login.logout") }} + <i class="button-icon icon-logout" /> {{ $t("login.logout") }} </a> </li> </ul> @@ -215,6 +215,10 @@ box-shadow: var(--panelShadow); background-color: $fallback--bg; background-color: var(--bg, $fallback--bg); + + .button-icon:before { + width: 1.1em; + } } .side-drawer-logo-wrapper { diff --git a/src/components/status/status.js b/src/components/status/status.js index 4fbd5ac3..c49e729c 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -13,10 +13,11 @@ import Timeago from '../timeago/timeago.vue' import StatusPopover from '../status_popover/status_popover.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import fileType from 'src/services/file_type/file_type.service' +import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js' import { filter, unescape, uniqBy } from 'lodash' -import { mapGetters } from 'vuex' +import { mapGetters, mapState } from 'vuex' const Status = { name: 'Status', @@ -32,7 +33,8 @@ const Status = { 'noHeading', 'inlineExpanded', 'showPinned', - 'inProfile' + 'inProfile', + 'profileUserId' ], data () { return { @@ -42,8 +44,8 @@ const Status = { showingTall: this.inConversation && this.focused, showingLongSubject: false, error: null, - expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject, - betterShadow: this.$store.state.interface.browserSupport.cssFilter + // not as computed because it sets the initial state which will be changed later + expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject } }, computed: { @@ -103,7 +105,7 @@ const Status = { return this.$store.state.statuses.allStatusesObject[this.status.id] }, loggedIn () { - return !!this.$store.state.users.currentUser + return !!this.currentUser }, muteWordHits () { const statusText = this.status.text.toLowerCase() @@ -114,7 +116,7 @@ const Status = { return hits }, - muted () { return !this.unmuted && ((!this.inProfile && this.status.user.muted) || (!this.inConversation && this.status.thread_muted) || this.muteWordHits.length > 0) }, + muted () { return !this.unmuted && ((!(this.inProfile && this.status.user.id === this.profileUserId) && this.status.user.muted) || (!this.inConversation && this.status.thread_muted) || this.muteWordHits.length > 0) }, hideFilteredStatuses () { return this.mergedConfig.hideFilteredStatuses }, @@ -163,7 +165,7 @@ const Status = { if (this.inConversation || !this.isReply) { return false } - if (this.status.user.id === this.$store.state.users.currentUser.id) { + if (this.status.user.id === this.currentUser.id) { return false } if (this.status.type === 'retweet') { @@ -178,7 +180,7 @@ const Status = { if (checkFollowing && taggedUser && taggedUser.following) { return false } - if (this.status.attentions[i].id === this.$store.state.users.currentUser.id) { + if (this.status.attentions[i].id === this.currentUser.id) { return false } } @@ -255,11 +257,41 @@ const Status = { maxThumbnails () { return this.mergedConfig.maxThumbnails }, + postBodyHtml () { + const html = this.status.statusnet_html + + if (this.mergedConfig.greentext) { + try { + if (html.includes('>')) { + // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works + return processHtml(html, (string) => { + if (string.includes('>') && + string + .replace(/<[^>]+?>/gi, '') // remove all tags + .replace(/@\w+/gi, '') // remove mentions (even failed ones) + .trim() + .startsWith('>')) { + return `<span class='greentext'>${string}</span>` + } else { + return string + } + }) + } else { + return html + } + } catch (e) { + console.err('Failed to process status html', e) + return html + } + } else { + return html + } + }, contentHtml () { if (!this.status.summary_html) { - return this.status.statusnet_html + return this.postBodyHtml } - return this.status.summary_html + '<br />' + this.status.statusnet_html + return this.status.summary_html + '<br />' + this.postBodyHtml }, combinedFavsAndRepeatsUsers () { // Use the status from the global status repository since favs and repeats are saved in it @@ -270,7 +302,7 @@ const Status = { return uniqBy(combinedUsers, 'id') }, ownStatus () { - return this.status.user.id === this.$store.state.users.currentUser.id + return this.status.user.id === this.currentUser.id }, tags () { return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ') @@ -278,7 +310,11 @@ const Status = { hidePostStats () { return this.mergedConfig.hidePostStats }, - ...mapGetters(['mergedConfig']) + ...mapGetters(['mergedConfig']), + ...mapState({ + betterShadow: state => state.interface.browserSupport.cssFilter, + currentUser: state => state.users.currentUser + }) }, components: { Attachment, diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 65778b2e..d291e762 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -606,7 +606,7 @@ $status-margin: 0.75em; height: 100%; mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat, linear-gradient(to top, white, white); - // Autoprefixed seem to ignore this one, and also syntax is different + /* Autoprefixed seem to ignore this one, and also syntax is different */ -webkit-mask-composite: xor; mask-composite: exclude; } @@ -752,7 +752,8 @@ $status-margin: 0.75em; } .greentext { - color: green; + color: $fallback--cGreen; + color: var(--cGreen, $fallback--cGreen); } .status-conversation { diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index f1d3903a..a6fba452 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -37,6 +37,7 @@ :collapsable="true" :pinned-status-ids-object="pinnedStatusIdsObject" :in-profile="inProfile" + :profile-user-id="userId" /> </template> <template v-for="status in timeline.visibleStatuses"> @@ -47,6 +48,7 @@ :status-id="status.id" :collapsable="true" :in-profile="inProfile" + :profile-user-id="userId" /> </template> </div> @@ -91,17 +93,4 @@ opacity: 1; } } - -.new-status-notification { - position:relative; - margin-top: -1px; - font-size: 1.1em; - border-width: 1px 0 0 0; - border-style: solid; - border-color: var(--border, $fallback--border); - padding: 10px; - z-index: 1; - background-color: $fallback--fg; - background-color: var(--panel, $fallback--fg); -} </style> diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index cc8a1ed6..2f649910 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -93,6 +93,12 @@ export default { const roleTitle = rights.admin ? 'admin' : 'moderator' return validRole && roleTitle }, + hideFollowsCount () { + return this.isOtherUser && this.user.hide_follows_count + }, + hideFollowersCount () { + return this.isOtherUser && this.user.hide_followers_count + }, ...mapGetters(['mergedConfig']) }, components: { @@ -143,6 +149,9 @@ export default { } this.$store.dispatch('setMedia', [attachment]) this.$store.dispatch('setCurrent', attachment) + }, + mentionUser () { + this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user }) } } } diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 6f3c958e..93d55fff 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -175,6 +175,14 @@ {{ $t('user_card.mute') }} </button> </div> + <div> + <button + class="btn btn-default btn-block" + @click="mentionUser" + > + {{ $t('user_card.mention') }} + </button> + </div> <ModerationTools v-if="loggedIn.role === "admin"" :user="user" @@ -208,14 +216,14 @@ @click.prevent="setProfileView('friends')" > <h5>{{ $t('user_card.followees') }}</h5> - <span>{{ user.friends_count }}</span> + <span>{{ hideFollowsCount ? $t('user_card.hidden') : user.friends_count }}</span> </div> <div class="user-count" @click.prevent="setProfileView('followers')" > <h5>{{ $t('user_card.followers') }}</h5> - <span>{{ user.followers_count }}</span> + <span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span> </div> </div> <!-- eslint-disable vue/no-v-html --> diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index 8c18cf49..3f1982a6 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -104,7 +104,7 @@ {{ $t('settings.hide_followers_count_description') }} </Checkbox> </p> - <p> + <p v-if="role === 'admin' || role === 'moderator'"> <Checkbox v-model="showRole"> <template v-if="role === 'admin'"> {{ $t('settings.show_admin_badge') }} diff --git a/src/i18n/en.json b/src/i18n/en.json index ead333c1..85146ef5 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -370,6 +370,8 @@ "false": "no", "true": "yes" }, + "fun": "Fun", + "greentext": "Meme arrows", "notifications": "Notifications", "notification_setting": "Receive notifications from:", "notification_setting_follows": "Users you follow", @@ -569,6 +571,7 @@ "followers": "Followers", "following": "Following!", "follows_you": "Follows you!", + "hidden": "Hidden", "its_you": "It's you!", "media": "Media", "mention": "Mention", diff --git a/src/i18n/ja.json b/src/i18n/ja_easy.json index 592a7257..592a7257 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja_easy.json diff --git a/src/i18n/messages.js b/src/i18n/messages.js index 89c8a8c8..c56ae205 100644 --- a/src/i18n/messages.js +++ b/src/i18n/messages.js @@ -23,8 +23,8 @@ const messages = { he: require('./he.json'), hu: require('./hu.json'), it: require('./it.json'), - ja: require('./ja.json'), - ja_pedantic: require('./ja_pedantic.json'), + ja: require('./ja_pedantic.json'), + ja_easy: require('./ja_easy.json'), ko: require('./ko.json'), nb: require('./nb.json'), nl: require('./nl.json'), diff --git a/src/i18n/ru.json b/src/i18n/ru.json index f8bcd996..19e10f1e 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -174,6 +174,8 @@ "name_bio": "Имя и описание", "new_email": "Новый email", "new_password": "Новый пароль", + "fun": "Потешное", + "greentext": "Мемные стрелочки", "notification_visibility": "Показывать уведомления", "notification_visibility_follows": "Подписки", "notification_visibility_likes": "Лайки", diff --git a/src/i18n/zh.json b/src/i18n/zh.json index 80c4e0d8..8d9462ab 100644 --- a/src/i18n/zh.json +++ b/src/i18n/zh.json @@ -111,7 +111,7 @@ }, "interactions": { "favs_repeats": "转发和收藏", - "follows": "新的关注着", + "follows": "新的关注者", "load_older": "加载更早的互动" }, "post_status": { diff --git a/src/modules/api.js b/src/modules/api.js index eb6a7980..1293e3c8 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -43,6 +43,13 @@ const api = { const fetcher = store.state.backendInteractor.startFetchingNotifications({ store }) store.commit('addFetcher', { fetcherName: 'notifications', fetcher }) }, + startFetchingFollowRequest (store) { + // Don't start fetching if we already are. + if (store.state.fetchers['followRequest']) return + + const fetcher = store.state.backendInteractor.startFetchingFollowRequest({ store }) + store.commit('addFetcher', { fetcherName: 'followRequest', fetcher }) + }, stopFetching (store, fetcherName) { const fetcher = store.state.fetchers[fetcherName] window.clearInterval(fetcher) diff --git a/src/modules/config.js b/src/modules/config.js index d4819ee8..329b4091 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -45,6 +45,7 @@ export const defaultState = { playVideosInModal: false, useOneClickNsfw: false, useContainFit: false, + greentext: undefined, // instance default hidePostStats: undefined, // instance default hideUserStats: undefined // instance default } diff --git a/src/modules/instance.js b/src/modules/instance.js index 7b0e0da4..625323b9 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -27,11 +27,13 @@ const defaultState = { scopeCopy: true, subjectLineBehavior: 'email', postContentType: 'text/plain', + hideSitename: false, nsfwCensorImage: undefined, vapidPublicKey: undefined, noAttachmentLinks: false, showFeaturesPanel: true, minimalScopesMode: false, + greentext: false, // Nasty stuff pleromaBackend: true, diff --git a/src/modules/users.js b/src/modules/users.js index 1c9ff5e8..14b2d8b5 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -434,6 +434,7 @@ const users = { store.dispatch('stopFetching', 'friends') store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) store.dispatch('stopFetching', 'notifications') + store.dispatch('stopFetching', 'followRequest') store.commit('clearNotifications') store.commit('resetStatuses') }) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 8f5eb416..68be0d50 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -22,7 +22,7 @@ const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes' const MFA_SETUP_OTP_URL = '/api/pleroma/accounts/mfa/setup/totp' const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp' -const MFA_DISABLE_OTP_URL = '/api/pleroma/account/mfa/totp' +const MFA_DISABLE_OTP_URL = '/api/pleroma/accounts/mfa/totp' const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials' const MASTODON_REGISTRATION_URL = '/api/v1/accounts' diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index d6617276..c16bd1f1 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -1,6 +1,7 @@ import apiService from '../api/api.service.js' import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js' import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js' +import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service' const backendInteractorService = credentials => { const fetchStatus = ({ id }) => { @@ -63,6 +64,10 @@ const backendInteractorService = credentials => { return notificationsFetcher.startFetching({ store, credentials }) } + const startFetchingFollowRequest = ({ store }) => { + return followRequestFetcher.startFetching({ store, credentials }) + } + // eslint-disable-next-line camelcase const tagUser = ({ screen_name }, tag) => { return apiService.tagUser({ screen_name, tag, credentials }) @@ -111,7 +116,6 @@ const backendInteractorService = credentials => { const subscribeUser = (id) => apiService.subscribeUser({ credentials, id }) const unsubscribeUser = (id) => apiService.unsubscribeUser({ credentials, id }) const fetchBlocks = () => apiService.fetchBlocks({ credentials }) - const fetchFollowRequests = () => apiService.fetchFollowRequests({ credentials }) const fetchOAuthTokens = () => apiService.fetchOAuthTokens({ credentials }) const revokeOAuthToken = (id) => apiService.revokeOAuthToken({ id, credentials }) const fetchPinnedStatuses = (id) => apiService.fetchPinnedStatuses({ credentials, id }) @@ -168,6 +172,7 @@ const backendInteractorService = credentials => { verifyCredentials: apiService.verifyCredentials, startFetchingTimeline, startFetchingNotifications, + startFetchingFollowRequest, fetchMutes, muteUser, unmuteUser, @@ -203,7 +208,6 @@ const backendInteractorService = credentials => { mfaSetupOTP, mfaConfirmOTP, mfaDisableOTP, - fetchFollowRequests, approveUser, denyUser, vote, diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 5f45660d..ca79df6f 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -46,6 +46,14 @@ export const parseUser = (data) => { output.description = data.note output.description_html = addEmojis(data.note, data.emojis) + output.fields = data.fields + output.fields_html = data.fields.map(field => { + return { + name: addEmojis(field.name, data.emojis), + value: addEmojis(field.value, data.emojis) + } + }) + // Utilize avatar_static for gif avatars? output.profile_image_url = data.avatar output.profile_image_url_original = data.avatar @@ -95,6 +103,7 @@ export const parseUser = (data) => { if (data.source) { output.description = data.source.note output.default_scope = data.source.privacy + output.fields = data.source.fields if (data.source.pleroma) { output.no_rich_text = data.source.pleroma.no_rich_text output.show_role = data.source.pleroma.show_role diff --git a/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js b/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js new file mode 100644 index 00000000..de6f20ef --- /dev/null +++ b/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js @@ -0,0 +1,94 @@ +/** + * This is a tiny purpose-built HTML parser/processor. This basically detects any type of visual newline and + * allows it to be processed, useful for greentexting, mostly + * + * known issue: doesn't handle CDATA so nested CDATA might not work well + * + * @param {Object} input - input data + * @param {(string) => string} processor - function that will be called on every line + * @return {string} processed html + */ +export const processHtml = (html, processor) => { + const handledTags = new Set(['p', 'br', 'div']) + const openCloseTags = new Set(['p', 'div']) + + let buffer = '' // Current output buffer + const level = [] // How deep we are in tags and which tags were there + let textBuffer = '' // Current line content + let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag + + // Extracts tag name from tag, i.e. <span a="b"> => span + const getTagName = (tag) => { + const result = /(?:<\/(\w+)>|<(\w+)\s?[^/]*?\/?>)/gi.exec(tag) + return result && (result[1] || result[2]) + } + + const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer + if (textBuffer.trim().length > 0) { + buffer += processor(textBuffer) + } else { + buffer += textBuffer + } + textBuffer = '' + } + + const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing + flush() + buffer += tag + } + + const handleOpen = (tag) => { // handles opening tags + flush() + buffer += tag + level.push(tag) + } + + const handleClose = (tag) => { // handles closing tags + flush() + buffer += tag + if (level[level.length - 1] === tag) { + level.pop() + } + } + + for (let i = 0; i < html.length; i++) { + const char = html[i] + if (char === '<' && tagBuffer === null) { + tagBuffer = char + } else if (char !== '>' && tagBuffer !== null) { + tagBuffer += char + } else if (char === '>' && tagBuffer !== null) { + tagBuffer += char + const tagFull = tagBuffer + tagBuffer = null + const tagName = getTagName(tagFull) + if (handledTags.has(tagName)) { + if (tagName === 'br') { + handleBr(tagFull) + } else if (openCloseTags.has(tagName)) { + if (tagFull[1] === '/') { + handleClose(tagFull) + } else if (tagFull[tagFull.length - 2] === '/') { + // self-closing + handleBr(tagFull) + } else { + handleOpen(tagFull) + } + } + } else { + textBuffer += tagFull + } + } else if (char === '\n') { + handleBr(char) + } else { + textBuffer += char + } + } + if (tagBuffer) { + textBuffer += tagBuffer + } + + flush() + + return buffer +} |
