Merge remote-tracking branch 'upstream/develop' into themes-accent

* upstream/develop: (33 commits)
  add emoji reactions to changelog
  fix emoji reaction classes broken in develop
  review changes
  Fix password and email update
  remove unnecessary anonymous function
  Apply suggestion to src/services/api/api.service.js
  remove logs/commented code
  remove favs count from react button
  remove mock data
  change emoji reactions to use new format
  Added polyfills for EventTarget (needed for Safari) and CustomEvent (needed for IE)
  Fix missing TWKN when logged in, but server is set to private mode.
  Fix follower request fetching
  Add domain mutes to changelog
  Implement domain mutes v2
  change changelog to reflect actual release history of frontend
  Fix #750 , fix error messages and captcha resetting
  Optimize Notifications Rendering
  update CHANGELOG
  Use last seen notif instead of first unseen notif for sinceId
  ...
This commit is contained in:
Henry Jameson 2020-01-28 20:44:13 +02:00
commit f0c4bb1193
49 changed files with 840 additions and 104 deletions

View file

@ -150,6 +150,7 @@ const conversation = {
if (!id) return
this.highlight = id
this.$store.dispatch('fetchFavsAndRepeats', id)
this.$store.dispatch('fetchEmojiReactionsBy', id)
},
getHighlight () {
return this.isExpanded ? this.highlight : null

View file

@ -0,0 +1,15 @@
import ProgressButton from '../progress_button/progress_button.vue'
const DomainMuteCard = {
props: ['domain'],
components: {
ProgressButton
},
methods: {
unmuteDomain () {
return this.$store.dispatch('unmuteDomain', this.domain)
}
}
}
export default DomainMuteCard

View file

@ -0,0 +1,38 @@
<template>
<div class="domain-mute-card">
<div class="domain-mute-card-domain">
{{ domain }}
</div>
<ProgressButton
:click="unmuteDomain"
class="btn btn-default"
>
{{ $t('domain_mute_card.unmute') }}
<template slot="progress">
{{ $t('domain_mute_card.unmute_progress') }}
</template>
</ProgressButton>
</div>
</template>
<script src="./domain_mute_card.js"></script>
<style lang="scss">
.domain-mute-card {
flex: 1 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.6em 1em 0.6em 0;
&-domain {
margin-right: 1em;
overflow: hidden;
text-overflow: ellipsis;
}
button {
width: 10em;
}
}
</style>

View file

@ -0,0 +1,32 @@
const EmojiReactions = {
name: 'EmojiReactions',
props: ['status'],
computed: {
emojiReactions () {
return this.status.emoji_reactions
}
},
methods: {
reactedWith (emoji) {
const user = this.$store.state.users.currentUser
const reaction = this.status.emoji_reactions.find(r => r.emoji === emoji)
return reaction.accounts && reaction.accounts.find(u => u.id === user.id)
},
reactWith (emoji) {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
},
unreact (emoji) {
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
},
emojiOnClick (emoji, event) {
if (this.reactedWith(emoji)) {
this.unreact(emoji)
} else {
this.reactWith(emoji)
}
}
}
}
export default EmojiReactions

View file

@ -0,0 +1,49 @@
<template>
<div class="emoji-reactions">
<button
v-for="(reaction) in emojiReactions"
:key="reaction.emoji"
class="emoji-reaction btn btn-default"
:class="{ 'picked-reaction': reactedWith(reaction.emoji) }"
@click="emojiOnClick(reaction.emoji, $event)"
>
<span class="reaction-emoji">{{ reaction.emoji }}</span>
<span>{{ reaction.count }}</span>
</button>
</div>
</template>
<script src="./emoji_reactions.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
.emoji-reactions {
display: flex;
margin-top: 0.25em;
flex-wrap: wrap;
}
.emoji-reaction {
padding: 0 0.5em;
margin-right: 0.5em;
margin-top: 0.5em;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
.reaction-emoji {
width: 1.25em;
margin-right: 0.25em;
}
&:focus {
outline: none;
}
}
.picked-reaction {
border: 1px solid var(--link, $fallback--link);
margin-left: -1px; // offset the border, can't use inset shadows either
margin-right: calc(0.5em - 1px);
}
</style>

View file

@ -3,7 +3,8 @@ import Notifications from '../notifications/notifications.vue'
const tabModeDict = {
mentions: ['mention'],
'likes+repeats': ['repeat', 'like'],
follows: ['follow']
follows: ['follow'],
moves: ['move']
}
const Interactions = {

View file

@ -21,6 +21,10 @@
key="follows"
:label="$t('interactions.follows')"
/>
<span
key="moves"
:label="$t('interactions.moves')"
/>
</tab-switcher>
<Notifications
ref="notifications"

View file

@ -3,7 +3,7 @@ import { mapState } from 'vuex'
const NavPanel = {
created () {
if (this.currentUser && this.currentUser.locked) {
this.$store.dispatch('startFetchingFollowRequest')
this.$store.dispatch('startFetchingFollowRequests')
}
},
computed: mapState({

View file

@ -33,7 +33,7 @@
<i class="button-icon icon-users" /> {{ $t("nav.public_tl") }}
</router-link>
</li>
<li v-if="federating && !privateMode">
<li v-if="federating && (currentUser || !privateMode)">
<router-link :to="{ name: 'public-external-timeline' }">
<i class="button-icon icon-globe" /> {{ $t("nav.twkn") }}
</router-link>

View file

@ -43,18 +43,18 @@ const Notification = {
const user = this.notification.from_profile
return highlightStyle(highlight[user.screen_name])
},
userInStore () {
return this.$store.getters.findUser(this.notification.from_profile.id)
},
user () {
if (this.userInStore) {
return this.userInStore
}
return this.notification.from_profile
return this.$store.getters.findUser(this.notification.from_profile.id)
},
userProfileLink () {
return this.generateUserProfileLink(this.user)
},
targetUser () {
return this.$store.getters.findUser(this.notification.target.id)
},
targetUserProfileLink () {
return this.generateUserProfileLink(this.targetUser)
},
needMute () {
return this.user.muted
}

View file

@ -74,9 +74,13 @@
<i class="fa icon-user-plus lit" />
<small>{{ $t('notifications.followed_you') }}</small>
</span>
<span v-if="notification.type === 'move'">
<i class="fa icon-arrow-curved lit" />
<small>{{ $t('notifications.migrated_to') }}</small>
</span>
</div>
<div
v-if="notification.type === 'follow'"
v-if="notification.type === 'follow' || notification.type === 'move'"
class="timeago"
>
<span class="faint">
@ -115,6 +119,14 @@
@{{ notification.from_profile.screen_name }}
</router-link>
</div>
<div
v-else-if="notification.type === 'move'"
class="move-text"
>
<router-link :to="targetUserProfileLink">
@{{ notification.target.screen_name }}
</router-link>
</div>
<template v-else>
<status
class="faint"

View file

@ -2,10 +2,12 @@ import Notification from '../notification/notification.vue'
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
import {
notificationsFromStore,
visibleNotificationsFromStore,
filteredNotificationsFromStore,
unseenNotificationsFromStore
} from '../../services/notification_utils/notification_utils.js'
const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30
const Notifications = {
props: {
// Disables display of panel header
@ -18,7 +20,11 @@ const Notifications = {
},
data () {
return {
bottomedOut: false
bottomedOut: false,
// How many seen notifications to display in the list. The more there are,
// the heavier the page becomes. This count is increased when loading
// older notifications, and cut back to default whenever hitting "Read!".
seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
}
},
computed: {
@ -34,14 +40,17 @@ const Notifications = {
unseenNotifications () {
return unseenNotificationsFromStore(this.$store)
},
visibleNotifications () {
return visibleNotificationsFromStore(this.$store, this.filterMode)
filteredNotifications () {
return filteredNotificationsFromStore(this.$store, this.filterMode)
},
unseenCount () {
return this.unseenNotifications.length
},
loading () {
return this.$store.state.statuses.notifications.loading
},
notificationsToDisplay () {
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
}
},
components: {
@ -64,12 +73,21 @@ const Notifications = {
methods: {
markAsSeen () {
this.$store.dispatch('markNotificationsAsSeen')
this.seenToDisplayCount = DEFAULT_SEEN_TO_DISPLAY_COUNT
},
fetchOlderNotifications () {
if (this.loading) {
return
}
const seenCount = this.filteredNotifications.length - this.unseenCount
if (this.seenToDisplayCount < seenCount) {
this.seenToDisplayCount = Math.min(this.seenToDisplayCount + 20, seenCount)
return
} else if (this.seenToDisplayCount > seenCount) {
this.seenToDisplayCount = seenCount
}
const store = this.$store
const credentials = store.state.users.currentUser.credentials
store.commit('setNotificationsLoading', { value: true })
@ -82,6 +100,7 @@ const Notifications = {
if (notifs.length === 0) {
this.bottomedOut = true
}
this.seenToDisplayCount += notifs.length
})
}
}

View file

@ -79,7 +79,7 @@
}
}
.follow-text {
.follow-text, .move-text {
padding: 0.5em 0;
}
@ -154,6 +154,11 @@
color: var(--cOrange, $fallback--cOrange);
}
.icon-arrow-curved.lit {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
.status-content {
margin: 0;
max-height: 300px;

View file

@ -32,7 +32,7 @@
</div>
<div class="panel-body">
<div
v-for="notification in visibleNotifications"
v-for="notification in notificationsToDisplay"
:key="notification.id"
class="notification"
:class="{&quot;unseen&quot;: !minimalMode && !notification.seen}"

View file

@ -0,0 +1,43 @@
import { mapGetters } from 'vuex'
const ReactButton = {
props: ['status', 'loggedIn'],
data () {
return {
showTooltip: false,
filterWord: '',
popperOptions: {
modifiers: {
preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
}
}
}
},
methods: {
openReactionSelect () {
this.showTooltip = true
this.filterWord = ''
},
closeReactionSelect () {
this.showTooltip = false
},
addReaction (event, emoji) {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
this.closeReactionSelect()
}
},
computed: {
commonEmojis () {
return ['❤️', '😠', '👀', '😂', '🔥']
},
emojis () {
if (this.filterWord !== '') {
return this.$store.state.instance.emoji.filter(emoji => emoji.displayText.includes(this.filterWord))
}
return this.$store.state.instance.emoji || []
},
...mapGetters(['mergedConfig'])
}
}
export default ReactButton

View file

@ -0,0 +1,109 @@
<template>
<v-popover
:popper-options="popperOptions"
:open="showTooltip"
trigger="manual"
placement="top"
class="react-button-popover"
@hide="closeReactionSelect"
>
<div slot="popover">
<div class="reaction-picker-filter">
<input
v-model="filterWord"
:placeholder="$t('emoji.search_emoji')"
>
</div>
<div class="reaction-picker">
<span
v-for="emoji in commonEmojis"
:key="emoji"
class="emoji-button"
@click="addReaction($event, emoji)"
>
{{ emoji }}
</span>
<div class="reaction-picker-divider" />
<span
v-for="(emoji, key) in emojis"
:key="key"
class="emoji-button"
@click="addReaction($event, emoji.replacement)"
>
{{ emoji.replacement }}
</span>
<div class="reaction-bottom-fader" />
</div>
</div>
<div
v-if="loggedIn"
@click.prevent="openReactionSelect"
>
<i
class="icon-smile button-icon add-reaction-button"
:title="$t('tool_tip.add_reaction')"
/>
</div>
</v-popover>
</template>
<script src="./react_button.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
.reaction-picker-filter {
padding: 0.5em;
}
.reaction-picker-divider {
height: 1px;
width: 100%;
margin: 0.5em;
background-color: var(--border, $fallback--border);
}
.reaction-picker {
width: 10em;
height: 9em;
font-size: 1.5em;
overflow-y: scroll;
display: flex;
flex-wrap: wrap;
padding: 0.5em;
text-align: center;
align-content: flex-start;
user-select: none;
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
linear-gradient(to top, white, white);
transition: mask-size 150ms;
mask-size: 100% 20px, 100% 20px, auto;
// Autoprefixed seem to ignore this one, and also syntax is different
-webkit-mask-composite: xor;
mask-composite: exclude;
.emoji-button {
cursor: pointer;
flex-basis: 20%;
line-height: 1.5em;
align-content: center;
&:hover {
transform: scale(1.25);
}
}
}
.add-reaction-button {
cursor: pointer;
&:hover {
color: $fallback--text;
color: var(--text, $fallback--text);
}
}
</style>

View file

@ -63,7 +63,8 @@ const registration = {
await this.signUp(this.user)
this.$router.push({ name: 'friends' })
} catch (error) {
console.warn('Registration failed: ' + error)
console.warn('Registration failed: ', error)
this.setCaptcha()
}
}
},

View file

@ -170,7 +170,7 @@
<label
class="form--label"
for="captcha-label"
>{{ $t('captcha') }}</label>
>{{ $t('registration.captcha') }}</label>
<template v-if="['kocaptcha', 'native'].includes(captcha.type)">
<img

View file

@ -323,6 +323,11 @@
{{ $t('settings.notification_visibility_mentions') }}
</Checkbox>
</li>
<li>
<Checkbox v-model="notificationVisibility.moves">
{{ $t('settings.notification_visibility_moves') }}
</Checkbox>
</li>
</ul>
</div>
<div>

View file

@ -12,7 +12,7 @@ const SideDrawer = {
this.closeGesture = GestureService.swipeGesture(GestureService.DIRECTION_LEFT, this.toggleDrawer)
if (this.currentUser && this.currentUser.locked) {
this.$store.dispatch('startFetchingFollowRequest')
this.$store.dispatch('startFetchingFollowRequests')
}
},
components: { UserCard },

View file

@ -88,7 +88,7 @@
</router-link>
</li>
<li
v-if="federating && !privateMode"
v-if="federating && (currentUser || !privateMode)"
@click="toggleDrawer"
>
<router-link to="/main/all">

View file

@ -1,3 +1,4 @@
import map from 'lodash/map'
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const StaffPanel = {
@ -6,7 +7,7 @@ const StaffPanel = {
},
computed: {
staffAccounts () {
return this.$store.state.instance.staffAccounts
return map(this.$store.state.instance.staffAccounts, nickname => this.$store.getters.findUser(nickname)).filter(_ => _)
}
}
}

View file

@ -1,5 +1,6 @@
import Attachment from '../attachment/attachment.vue'
import FavoriteButton from '../favorite_button/favorite_button.vue'
import ReactButton from '../react_button/react_button.vue'
import RetweetButton from '../retweet_button/retweet_button.vue'
import Poll from '../poll/poll.vue'
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
@ -11,6 +12,7 @@ import LinkPreview from '../link-preview/link-preview.vue'
import AvatarList from '../avatar_list/avatar_list.vue'
import Timeago from '../timeago/timeago.vue'
import StatusPopover from '../status_popover/status_popover.vue'
import EmojiReactions from '../emoji_reactions/emoji_reactions.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'
@ -319,6 +321,7 @@ const Status = {
components: {
Attachment,
FavoriteButton,
ReactButton,
RetweetButton,
ExtraButtons,
PostStatusForm,
@ -329,7 +332,8 @@ const Status = {
LinkPreview,
AvatarList,
Timeago,
StatusPopover
StatusPopover,
EmojiReactions
},
methods: {
visibilityIcon (visibility) {

View file

@ -354,6 +354,10 @@
</div>
</transition>
<EmojiReactions
:status="status"
/>
<div
v-if="!noHeading && !isPreview"
class="status-actions media-body"
@ -382,6 +386,10 @@
:logged-in="loggedIn"
:status="status"
/>
<ReactButton
:logged-in="loggedIn"
:status="status"
/>
<extra-buttons
:status="status"
@onError="showError"

View file

@ -139,7 +139,7 @@ const Mfa = {
// fetch settings from server
async fetchSettings () {
let result = await this.backendInteractor.fetchSettingsMFA()
let result = await this.backendInteractor.settingsMFA()
if (result.error) return
this.settings = result.settings
this.settings.available = true

View file

@ -9,6 +9,7 @@ import ScopeSelector from '../scope_selector/scope_selector.vue'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
import BlockCard from '../block_card/block_card.vue'
import MuteCard from '../mute_card/mute_card.vue'
import DomainMuteCard from '../domain_mute_card/domain_mute_card.vue'
import SelectableList from '../selectable_list/selectable_list.vue'
import ProgressButton from '../progress_button/progress_button.vue'
import EmojiInput from '../emoji_input/emoji_input.vue'
@ -32,6 +33,12 @@ const MuteList = withSubscription({
childPropName: 'items'
})(SelectableList)
const DomainMuteList = withSubscription({
fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
childPropName: 'items'
})(SelectableList)
const UserSettings = {
data () {
return {
@ -67,7 +74,8 @@ const UserSettings = {
changedPassword: false,
changePasswordError: false,
activeTab: 'profile',
notificationSettings: this.$store.state.users.currentUser.notification_settings
notificationSettings: this.$store.state.users.currentUser.notification_settings,
newDomainToMute: ''
}
},
created () {
@ -80,10 +88,12 @@ const UserSettings = {
ImageCropper,
BlockList,
MuteList,
DomainMuteList,
EmojiInput,
Autosuggest,
BlockCard,
MuteCard,
DomainMuteCard,
ProgressButton,
Importer,
Exporter,
@ -297,7 +307,7 @@ const UserSettings = {
newPassword: this.changePasswordInputs[1],
newPasswordConfirmation: this.changePasswordInputs[2]
}
this.$store.state.api.backendInteractor.changePassword({ params })
this.$store.state.api.backendInteractor.changePassword(params)
.then((res) => {
if (res.status === 'success') {
this.changedPassword = true
@ -314,7 +324,7 @@ const UserSettings = {
email: this.newEmail,
password: this.changeEmailPassword
}
this.$store.state.api.backendInteractor.changeEmail({ params })
this.$store.state.api.backendInteractor.changeEmail(params)
.then((res) => {
if (res.status === 'success') {
this.changedEmail = true
@ -365,6 +375,13 @@ const UserSettings = {
unmuteUsers (ids) {
return this.$store.dispatch('unmuteUsers', ids)
},
unmuteDomains (domains) {
return this.$store.dispatch('unmuteDomains', domains)
},
muteDomain () {
return this.$store.dispatch('muteDomain', this.newDomainToMute)
.then(() => { this.newDomainToMute = '' })
},
identity (value) {
return value
}

View file

@ -509,59 +509,114 @@
</div>
<div :label="$t('settings.mutes_tab')">
<div class="profile-edit-usersearch-wrapper">
<Autosuggest
:filter="filterUnMutedUsers"
:query="queryUserIds"
:placeholder="$t('settings.search_user_to_mute')"
>
<MuteCard
slot-scope="row"
:user-id="row.item"
/>
</Autosuggest>
</div>
<MuteList
:refresh="true"
:get-key="identity"
>
<template
slot="header"
slot-scope="{selected}"
>
<div class="profile-edit-bulk-actions">
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default"
:click="() => muteUsers(selected)"
<tab-switcher>
<div label="Users">
<div class="profile-edit-usersearch-wrapper">
<Autosuggest
:filter="filterUnMutedUsers"
:query="queryUserIds"
:placeholder="$t('settings.search_user_to_mute')"
>
{{ $t('user_card.mute') }}
<template slot="progress">
{{ $t('user_card.mute_progress') }}
</template>
</ProgressButton>
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default"
:click="() => unmuteUsers(selected)"
<MuteCard
slot-scope="row"
:user-id="row.item"
/>
</Autosuggest>
</div>
<MuteList
:refresh="true"
:get-key="identity"
>
<template
slot="header"
slot-scope="{selected}"
>
{{ $t('user_card.unmute') }}
<div class="profile-edit-bulk-actions">
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default"
:click="() => muteUsers(selected)"
>
{{ $t('user_card.mute') }}
<template slot="progress">
{{ $t('user_card.mute_progress') }}
</template>
</ProgressButton>
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default"
:click="() => unmuteUsers(selected)"
>
{{ $t('user_card.unmute') }}
<template slot="progress">
{{ $t('user_card.unmute_progress') }}
</template>
</ProgressButton>
</div>
</template>
<template
slot="item"
slot-scope="{item}"
>
<MuteCard :user-id="item" />
</template>
<template slot="empty">
{{ $t('settings.no_mutes') }}
</template>
</MuteList>
</div>
<div :label="$t('settings.domain_mutes')">
<div class="profile-edit-domain-mute-form">
<input
v-model="newDomainToMute"
:placeholder="$t('settings.type_domains_to_mute')"
type="text"
@keyup.enter="muteDomain"
>
<ProgressButton
class="btn btn-default"
:click="muteDomain"
>
{{ $t('domain_mute_card.mute') }}
<template slot="progress">
{{ $t('user_card.unmute_progress') }}
{{ $t('domain_mute_card.mute_progress') }}
</template>
</ProgressButton>
</div>
</template>
<template
slot="item"
slot-scope="{item}"
>
<MuteCard :user-id="item" />
</template>
<template slot="empty">
{{ $t('settings.no_mutes') }}
</template>
</MuteList>
<DomainMuteList
:refresh="true"
:get-key="identity"
>
<template
slot="header"
slot-scope="{selected}"
>
<div class="profile-edit-bulk-actions">
<ProgressButton
v-if="selected.length > 0"
class="btn btn-default"
:click="() => unmuteDomains(selected)"
>
{{ $t('domain_mute_card.unmute') }}
<template slot="progress">
{{ $t('domain_mute_card.unmute_progress') }}
</template>
</ProgressButton>
</div>
</template>
<template
slot="item"
slot-scope="{item}"
>
<DomainMuteCard :domain="item" />
</template>
<template slot="empty">
{{ $t('settings.no_mutes') }}
</template>
</DomainMuteList>
</div>
</tab-switcher>
</div>
</tab-switcher>
</div>
@ -639,6 +694,18 @@
}
}
&-domain-mute-form {
padding: 1em;
display: flex;
flex-direction: column;
button {
align-self: flex-end;
margin-top: 1em;
width: 10em;
}
}
.setting-subitem {
margin-left: 1.75em;
}