Merge remote-tracking branch 'upstream/develop' into shigusegubu
* upstream/develop: (33 commits) fix js error on mute Apply suggestion to src/services/follow_manipulate/follow_manipulate.js Apply suggestion to src/services/follow_manipulate/follow_manipulate.js Apply suggestion to src/services/follow_manipulate/follow_manipulate.js Fix sent follow request detection fix english settings label regarding how to view videos fix extra buttons merge bug make size of gif image and preview equal css improvements do not unmount post status modal in desktop hide rich media preview image in case of broken image Handle JSONified errors while registering Focus on the search input when the search icon is clicked prevent scrolling top when click search input add zoom-in indication to avatar add zoom-in icon allow zooming avatar in profile panel header use $route instead of $router.currentRoute enlarge avatar in profile page update unit test ...
This commit is contained in:
commit
cf22cf778c
38 changed files with 326 additions and 101 deletions
|
@ -41,6 +41,7 @@
|
|||
<search-bar
|
||||
class="nav-icon mobile-hidden"
|
||||
@toggled="onSearchBarToggled"
|
||||
@click.stop.native
|
||||
/>
|
||||
<router-link
|
||||
class="mobile-hidden"
|
||||
|
@ -106,6 +107,7 @@
|
|||
:floating="true"
|
||||
class="floating-chat mobile-hidden"
|
||||
/>
|
||||
<MobilePostStatusModal />
|
||||
<UserReportingModal />
|
||||
<portal-target name="modal" />
|
||||
</div>
|
||||
|
|
|
@ -16,6 +16,16 @@ const ExtraButtons = {
|
|||
this.$store.dispatch('unpinStatus', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
muteConversation () {
|
||||
this.$store.dispatch('muteConversation', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
unmuteConversation () {
|
||||
this.$store.dispatch('unmuteConversation', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -30,9 +40,6 @@ const ExtraButtons = {
|
|||
},
|
||||
canPin () {
|
||||
return this.ownStatus && (this.status.visibility === 'public' || this.status.visibility === 'unlisted')
|
||||
},
|
||||
enabled () {
|
||||
return this.canPin || this.canDelete
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<v-popover
|
||||
v-if="enabled"
|
||||
trigger="click"
|
||||
placement="top"
|
||||
class="extra-button-popover"
|
||||
|
@ -9,6 +8,20 @@
|
|||
>
|
||||
<div slot="popover">
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
v-if="!status.muted"
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="muteConversation"
|
||||
>
|
||||
<i class="icon-eye-off" /><span>{{ $t("status.mute_conversation") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="status.muted"
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="unmuteConversation"
|
||||
>
|
||||
<i class="icon-eye-off" /><span>{{ $t("status.unmute_conversation") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="!status.pinned && canPin"
|
||||
v-close-popover
|
||||
|
|
|
@ -61,13 +61,17 @@
|
|||
}
|
||||
|
||||
&.contain-fit {
|
||||
img, video {
|
||||
img,
|
||||
video,
|
||||
canvas {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
&.cover-fit {
|
||||
img, video {
|
||||
img,
|
||||
video,
|
||||
canvas {
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,11 @@ const LinkPreview = {
|
|||
'size',
|
||||
'nsfw'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
imageLoaded: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
useImage () {
|
||||
// Currently BE shoudn't give cards if tagged NSFW, this is a bit paranoid
|
||||
|
@ -15,6 +20,15 @@ const LinkPreview = {
|
|||
useDescription () {
|
||||
return this.card.description && /\S/.test(this.card.description)
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.useImage) {
|
||||
const newImg = new Image()
|
||||
newImg.onload = () => {
|
||||
this.imageLoaded = true
|
||||
}
|
||||
newImg.src = this.card.image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
rel="noopener"
|
||||
>
|
||||
<div
|
||||
v-if="useImage"
|
||||
v-if="useImage && imageLoaded"
|
||||
class="card-image"
|
||||
:class="{ 'small-image': size === 'small' }"
|
||||
>
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import SideDrawer from '../side_drawer/side_drawer.vue'
|
||||
import Notifications from '../notifications/notifications.vue'
|
||||
import MobilePostStatusModal from '../mobile_post_status_modal/mobile_post_status_modal.vue'
|
||||
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||
import GestureService from '../../services/gesture_service/gesture_service'
|
||||
|
||||
const MobileNav = {
|
||||
components: {
|
||||
SideDrawer,
|
||||
Notifications,
|
||||
MobilePostStatusModal
|
||||
Notifications
|
||||
},
|
||||
data: () => ({
|
||||
notificationsCloseGesture: undefined,
|
||||
|
|
|
@ -70,7 +70,6 @@
|
|||
ref="sideDrawer"
|
||||
:logout="logout"
|
||||
/>
|
||||
<MobilePostStatusModal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -34,14 +34,19 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.post-form-modal-view {
|
||||
max-height: 100%;
|
||||
display: block;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.post-form-modal-panel {
|
||||
flex-shrink: 0;
|
||||
margin: 25% 0 4em 0;
|
||||
margin-top: 25%;
|
||||
margin-bottom: 2em;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
|
||||
@media (orientation: landscape) {
|
||||
margin-top: 8%;
|
||||
}
|
||||
}
|
||||
|
||||
.new-status-button {
|
||||
|
|
|
@ -20,6 +20,11 @@ const SearchBar = {
|
|||
toggleHidden () {
|
||||
this.hidden = !this.hidden
|
||||
this.$emit('toggled', this.hidden)
|
||||
this.$nextTick(() => {
|
||||
if (!this.hidden) {
|
||||
this.$refs.searchInput.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,20 @@
|
|||
import Status from '../status/status.vue'
|
||||
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
|
||||
import Conversation from '../conversation/conversation.vue'
|
||||
import { throttle } from 'lodash'
|
||||
import { throttle, keyBy } from 'lodash'
|
||||
|
||||
export const getExcludedStatusIdsByPinning = (statuses, pinnedStatusIds) => {
|
||||
const ids = []
|
||||
if (pinnedStatusIds && pinnedStatusIds.length > 0) {
|
||||
for (let status of statuses) {
|
||||
if (!pinnedStatusIds.includes(status.id)) {
|
||||
break
|
||||
}
|
||||
ids.push(status.id)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
const Timeline = {
|
||||
props: [
|
||||
|
@ -11,7 +24,8 @@ const Timeline = {
|
|||
'userId',
|
||||
'tag',
|
||||
'embedded',
|
||||
'count'
|
||||
'count',
|
||||
'pinnedStatusIds'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
|
@ -39,6 +53,12 @@ const Timeline = {
|
|||
body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []),
|
||||
footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : [])
|
||||
}
|
||||
},
|
||||
// id map of statuses which need to be hidden in the main list due to pinning logic
|
||||
excludedStatusIdsObject () {
|
||||
const ids = getExcludedStatusIdsByPinning(this.timeline.visibleStatuses, this.pinnedStatusIds)
|
||||
// Convert id array to object
|
||||
return keyBy(ids, id => id)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -28,13 +28,25 @@
|
|||
</div>
|
||||
<div :class="classes.body">
|
||||
<div class="timeline">
|
||||
<conversation
|
||||
v-for="status in timeline.visibleStatuses"
|
||||
:key="status.id"
|
||||
class="status-fadein"
|
||||
:statusoid="status"
|
||||
:collapsable="true"
|
||||
/>
|
||||
<template v-for="statusId in pinnedStatusIds">
|
||||
<conversation
|
||||
v-if="timeline.statusesObject[statusId]"
|
||||
:key="statusId + '-pinned'"
|
||||
class="status-fadein"
|
||||
:statusoid="timeline.statusesObject[statusId]"
|
||||
:collapsable="true"
|
||||
:show-pinned="true"
|
||||
/>
|
||||
</template>
|
||||
<template v-for="status in timeline.visibleStatuses">
|
||||
<conversation
|
||||
v-if="!excludedStatusIdsObject[status.id]"
|
||||
:key="status.id"
|
||||
class="status-fadein"
|
||||
:statusoid="status"
|
||||
:collapsable="true"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="classes.footer">
|
||||
|
|
|
@ -7,7 +7,7 @@ import { requestFollow, requestUnfollow } from '../../services/follow_manipulate
|
|||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
export default {
|
||||
props: [ 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered' ],
|
||||
props: [ 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar' ],
|
||||
data () {
|
||||
return {
|
||||
followRequestInProgress: false,
|
||||
|
@ -162,6 +162,14 @@ export default {
|
|||
},
|
||||
reportUser () {
|
||||
this.$store.dispatch('openUserReportingModal', this.user.id)
|
||||
},
|
||||
zoomAvatar () {
|
||||
const attachment = {
|
||||
url: this.user.profile_image_url_original,
|
||||
mimetype: 'image'
|
||||
}
|
||||
this.$store.dispatch('setMedia', [attachment])
|
||||
this.$store.dispatch('setCurrent', attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,23 @@
|
|||
<div class="panel-heading">
|
||||
<div class="user-info">
|
||||
<div class="container">
|
||||
<router-link :to="userProfileLink(user)">
|
||||
<a
|
||||
v-if="allowZoomingAvatar"
|
||||
class="user-info-avatar-link"
|
||||
@click="zoomAvatar"
|
||||
>
|
||||
<UserAvatar
|
||||
:better-shadow="betterShadow"
|
||||
:user="user"
|
||||
/>
|
||||
<div class="user-info-avatar-link-overlay">
|
||||
<i class="button-icon icon-zoom-in" />
|
||||
</div>
|
||||
</a>
|
||||
<router-link
|
||||
v-else
|
||||
:to="userProfileLink(user)"
|
||||
>
|
||||
<UserAvatar
|
||||
:better-shadow="betterShadow"
|
||||
:user="user"
|
||||
|
@ -351,6 +367,7 @@
|
|||
.container {
|
||||
padding: 16px 0 6px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
max-height: 56px;
|
||||
|
||||
.avatar {
|
||||
|
@ -372,6 +389,35 @@
|
|||
}
|
||||
}
|
||||
|
||||
&-avatar-link {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&-overlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: $fallback--avatarRadius;
|
||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||
opacity: 0;
|
||||
transition: opacity .2s ease;
|
||||
|
||||
i {
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover &-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.usersettings {
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
:user="user"
|
||||
:switcher="true"
|
||||
:selected="timeline.viewing"
|
||||
:allow-zooming-avatar="true"
|
||||
rounded="top"
|
||||
/>
|
||||
<tab-switcher
|
||||
|
@ -15,25 +16,14 @@
|
|||
:render-only-focused="true"
|
||||
>
|
||||
<div :label="$t('user_card.statuses')">
|
||||
<div class="timeline">
|
||||
<template v-for="statusId in user.pinnedStatuseIds">
|
||||
<Conversation
|
||||
v-if="timeline.statusesObject[statusId]"
|
||||
:key="statusId"
|
||||
class="status-fadein"
|
||||
:statusoid="timeline.statusesObject[statusId]"
|
||||
:collapsable="true"
|
||||
:show-pinned="true"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<Timeline
|
||||
:count="user.statuses_count"
|
||||
:embedded="true"
|
||||
:title="$t('user_profile.timeline_title')"
|
||||
:timeline="timeline"
|
||||
:timeline-name="'user'"
|
||||
timeline-name="user"
|
||||
:user-id="userId"
|
||||
:pinned-status-ids="user.pinnedStatusIds"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
@ -262,7 +262,7 @@
|
|||
"loop_video": "Loop videos",
|
||||
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
|
||||
"mutes_tab": "Mutes",
|
||||
"play_videos_in_modal": "Play videos directly in the media viewer",
|
||||
"play_videos_in_modal": "Play videos in a popup frame",
|
||||
"use_contain_fit": "Don't crop the attachment in thumbnails",
|
||||
"name": "Name",
|
||||
"name_bio": "Name & Bio",
|
||||
|
@ -508,7 +508,9 @@
|
|||
"pinned": "Pinned",
|
||||
"delete_confirm": "Do you really want to delete this status?",
|
||||
"reply_to": "Reply to",
|
||||
"replies_list": "Replies:"
|
||||
"replies_list": "Replies:",
|
||||
"mute_conversation": "Mute conversation",
|
||||
"unmute_conversation": "Unmute conversation"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "Approve",
|
||||
|
|
|
@ -106,6 +106,9 @@
|
|||
"expired": "La encuesta terminó hace {0}",
|
||||
"not_enough_options": "Muy pocas opciones únicas en la encuesta"
|
||||
},
|
||||
"stickers": {
|
||||
"add_sticker": "Añadir Pegatina"
|
||||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "Favoritos y Repetidos",
|
||||
"follows": "Nuevos seguidores",
|
||||
|
|
|
@ -278,8 +278,15 @@
|
|||
"status": {
|
||||
"favorites": "Tykkäykset",
|
||||
"repeats": "Toistot",
|
||||
"delete": "Poista",
|
||||
"pin": "Kiinnitä profiiliisi",
|
||||
"unpin": "Poista kiinnitys",
|
||||
"pinned": "Kiinnitetty",
|
||||
"delete_confirm": "Haluatko varmasti postaa viestin?",
|
||||
"reply_to": "Vastaus",
|
||||
"replies_list": "Vastaukset:"
|
||||
"replies_list": "Vastaukset:",
|
||||
"mute_conversation": "Hiljennä keskustelu",
|
||||
"unmute_conversation": "Poista hiljennys"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "Hyväksy",
|
||||
|
|
|
@ -430,6 +430,10 @@ export const mutations = {
|
|||
const newStatus = state.allStatusesObject[status.id]
|
||||
newStatus.pinned = status.pinned
|
||||
},
|
||||
setMuted (state, status) {
|
||||
const newStatus = state.allStatusesObject[status.id]
|
||||
newStatus.muted = status.muted
|
||||
},
|
||||
setRetweeted (state, { status, value }) {
|
||||
const newStatus = state.allStatusesObject[status.id]
|
||||
|
||||
|
@ -564,6 +568,14 @@ const statuses = {
|
|||
rootState.api.backendInteractor.unpinOwnStatus(statusId)
|
||||
.then((status) => commit('setPinned', status))
|
||||
},
|
||||
muteConversation ({ rootState, commit }, statusId) {
|
||||
return rootState.api.backendInteractor.muteConversation(statusId)
|
||||
.then((status) => commit('setMuted', status))
|
||||
},
|
||||
unmuteConversation ({ rootState, commit }, statusId) {
|
||||
return rootState.api.backendInteractor.unmuteConversation(statusId)
|
||||
.then((status) => commit('setMuted', status))
|
||||
},
|
||||
retweet ({ rootState, commit }, status) {
|
||||
// Optimistic retweeting...
|
||||
commit('setRetweeted', { status, value: true })
|
||||
|
|
|
@ -3,7 +3,6 @@ import oauthApi from '../services/new_api/oauth.js'
|
|||
import { compact, map, each, merge, last, concat, uniq } from 'lodash'
|
||||
import { set } from 'vue'
|
||||
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
|
||||
import { humanizeErrors } from './errors'
|
||||
|
||||
// TODO: Unify with mergeOrAdd in statuses.js
|
||||
export const mergeOrAdd = (arr, obj, item) => {
|
||||
|
@ -167,11 +166,11 @@ export const mutations = {
|
|||
},
|
||||
setPinned (state, status) {
|
||||
const user = state.usersObject[status.user.id]
|
||||
const index = user.pinnedStatuseIds.indexOf(status.id)
|
||||
const index = user.pinnedStatusIds.indexOf(status.id)
|
||||
if (status.pinned && index === -1) {
|
||||
user.pinnedStatuseIds.push(status.id)
|
||||
user.pinnedStatusIds.push(status.id)
|
||||
} else if (!status.pinned && index !== -1) {
|
||||
user.pinnedStatuseIds.splice(index, 1)
|
||||
user.pinnedStatusIds.splice(index, 1)
|
||||
}
|
||||
},
|
||||
setUserForStatus (state, status) {
|
||||
|
@ -382,16 +381,8 @@ const users = {
|
|||
store.dispatch('loginUser', data.access_token)
|
||||
} catch (e) {
|
||||
let errors = e.message
|
||||
// replace ap_id with username
|
||||
if (typeof errors === 'object') {
|
||||
if (errors.ap_id) {
|
||||
errors.username = errors.ap_id
|
||||
delete errors.ap_id
|
||||
}
|
||||
errors = humanizeErrors(errors)
|
||||
}
|
||||
store.commit('signUpFailure', errors)
|
||||
throw Error(errors)
|
||||
throw e
|
||||
}
|
||||
},
|
||||
async getCaptcha (store) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { each, map, concat, last } from 'lodash'
|
||||
import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
|
||||
import 'whatwg-fetch'
|
||||
import { StatusCodeError } from '../errors/errors'
|
||||
import { RegistrationError, StatusCodeError } from '../errors/errors'
|
||||
|
||||
/* eslint-env browser */
|
||||
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
|
||||
|
@ -67,6 +67,8 @@ const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials'
|
|||
const MASTODON_REPORT_USER_URL = '/api/v1/reports'
|
||||
const MASTODON_PIN_OWN_STATUS = id => `/api/v1/statuses/${id}/pin`
|
||||
const MASTODON_UNPIN_OWN_STATUS = id => `/api/v1/statuses/${id}/unpin`
|
||||
const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
|
||||
const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
|
||||
const MASTODON_SEARCH_2 = `/api/v2/search`
|
||||
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
|
||||
|
||||
|
@ -199,12 +201,11 @@ const register = ({ params, credentials }) => {
|
|||
...rest
|
||||
})
|
||||
})
|
||||
.then((response) => [response.ok, response])
|
||||
.then(([ok, response]) => {
|
||||
if (ok) {
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json()
|
||||
} else {
|
||||
return response.json().then((error) => { throw new Error(error) })
|
||||
return response.json().then((error) => { throw new RegistrationError(error) })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -253,6 +254,16 @@ const unpinOwnStatus = ({ id, credentials }) => {
|
|||
.then((data) => parseStatus(data))
|
||||
}
|
||||
|
||||
const muteConversation = ({ id, credentials }) => {
|
||||
return promisedRequest({ url: MASTODON_MUTE_CONVERSATION(id), credentials, method: 'POST' })
|
||||
.then((data) => parseStatus(data))
|
||||
}
|
||||
|
||||
const unmuteConversation = ({ id, credentials }) => {
|
||||
return promisedRequest({ url: MASTODON_UNMUTE_CONVERSATION(id), credentials, method: 'POST' })
|
||||
.then((data) => parseStatus(data))
|
||||
}
|
||||
|
||||
const blockUser = ({ id, credentials }) => {
|
||||
return fetch(MASTODON_BLOCK_USER_URL(id), {
|
||||
headers: authHeaders(credentials),
|
||||
|
@ -921,6 +932,8 @@ const apiService = {
|
|||
unfollowUser,
|
||||
pinOwnStatus,
|
||||
unpinOwnStatus,
|
||||
muteConversation,
|
||||
unmuteConversation,
|
||||
blockUser,
|
||||
unblockUser,
|
||||
fetchUser,
|
||||
|
|
|
@ -117,6 +117,8 @@ const backendInteractorService = credentials => {
|
|||
const fetchPinnedStatuses = (id) => apiService.fetchPinnedStatuses({ credentials, id })
|
||||
const pinOwnStatus = (id) => apiService.pinOwnStatus({ credentials, id })
|
||||
const unpinOwnStatus = (id) => apiService.unpinOwnStatus({ credentials, id })
|
||||
const muteConversation = (id) => apiService.muteConversation({ credentials, id })
|
||||
const unmuteConversation = (id) => apiService.unmuteConversation({ credentials, id })
|
||||
|
||||
const getCaptcha = () => apiService.getCaptcha()
|
||||
const register = (params) => apiService.register({ credentials, params })
|
||||
|
@ -178,6 +180,8 @@ const backendInteractorService = credentials => {
|
|||
fetchPinnedStatuses,
|
||||
pinOwnStatus,
|
||||
unpinOwnStatus,
|
||||
muteConversation,
|
||||
unmuteConversation,
|
||||
tagUser,
|
||||
untagUser,
|
||||
addRight,
|
||||
|
|
|
@ -65,6 +65,7 @@ export const parseUser = (data) => {
|
|||
|
||||
if (relationship) {
|
||||
output.follows_you = relationship.followed_by
|
||||
output.requested = relationship.requested
|
||||
output.following = relationship.following
|
||||
output.statusnet_blocking = relationship.blocking
|
||||
output.muted = relationship.muting
|
||||
|
@ -152,7 +153,7 @@ export const parseUser = (data) => {
|
|||
output.statuses_count = data.statuses_count
|
||||
output.friendIds = []
|
||||
output.followerIds = []
|
||||
output.pinnedStatuseIds = []
|
||||
output.pinnedStatusIds = []
|
||||
|
||||
if (data.pleroma) {
|
||||
output.follow_request_count = data.pleroma.follow_request_count
|
||||
|
@ -240,6 +241,7 @@ export const parseStatus = (data) => {
|
|||
output.external_url = data.url
|
||||
output.poll = data.poll
|
||||
output.pinned = data.pinned
|
||||
output.muted = data.muted
|
||||
} else {
|
||||
output.favorited = data.favorited
|
||||
output.fave_num = data.fave_num
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { humanizeErrors } from '../../modules/errors'
|
||||
|
||||
export function StatusCodeError (statusCode, body, options, response) {
|
||||
this.name = 'StatusCodeError'
|
||||
this.statusCode = statusCode
|
||||
|
@ -12,3 +14,36 @@ export function StatusCodeError (statusCode, body, options, response) {
|
|||
}
|
||||
StatusCodeError.prototype = Object.create(Error.prototype)
|
||||
StatusCodeError.prototype.constructor = StatusCodeError
|
||||
|
||||
export class RegistrationError extends Error {
|
||||
constructor (error) {
|
||||
super()
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this)
|
||||
}
|
||||
|
||||
try {
|
||||
// the error is probably a JSON object with a single key, "errors", whose value is another JSON object containing the real errors
|
||||
if (typeof error === 'string') {
|
||||
error = JSON.parse(error)
|
||||
if (error.hasOwnProperty('error')) {
|
||||
error = JSON.parse(error.error)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof error === 'object') {
|
||||
// replace ap_id with username
|
||||
if (error.ap_id) {
|
||||
error.username = error.ap_id
|
||||
delete error.ap_id
|
||||
}
|
||||
this.message = humanizeErrors(error)
|
||||
} else {
|
||||
this.message = error
|
||||
}
|
||||
} catch (e) {
|
||||
// can't parse it, so just treat it like a string
|
||||
this.message = error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,17 +2,17 @@ const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => {
|
|||
setTimeout(() => {
|
||||
store.state.api.backendInteractor.fetchUser({ id: user.id })
|
||||
.then((user) => store.commit('addNewUsers', [user]))
|
||||
.then(() => resolve([user.following, attempt]))
|
||||
.then(() => resolve([user.following, user.requested, user.locked, attempt]))
|
||||
.catch((e) => reject(e))
|
||||
}, 500)
|
||||
}).then(([following, attempt]) => {
|
||||
if (!following && attempt <= 3) {
|
||||
}).then(([following, sent, locked, attempt]) => {
|
||||
if (!following && !(locked && sent) && attempt <= 3) {
|
||||
// If we BE reports that we still not following that user - retry,
|
||||
// increment attempts by one
|
||||
return fetchUser(++attempt, user, store)
|
||||
} else {
|
||||
// If we run out of attempts, just return whatever status is.
|
||||
return following
|
||||
return sent
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -21,14 +21,10 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => {
|
|||
.then((updated) => {
|
||||
store.commit('updateUserRelationship', [updated])
|
||||
|
||||
// For locked users we just mark it that we sent the follow request
|
||||
if (updated.locked) {
|
||||
resolve({ sent: true })
|
||||
}
|
||||
|
||||
if (updated.following) {
|
||||
// If we get result immediately, just stop.
|
||||
resolve({ sent: false })
|
||||
if (updated.following || (user.locked && user.requested)) {
|
||||
// If we get result immediately or the account is locked, just stop.
|
||||
resolve({ sent: updated.requested })
|
||||
return
|
||||
}
|
||||
|
||||
// But usually we don't get result immediately, so we ask server
|
||||
|
@ -39,14 +35,8 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => {
|
|||
// Recursive Promise, it will call itself up to 3 times.
|
||||
|
||||
return fetchUser(1, user, store)
|
||||
.then((following) => {
|
||||
if (following) {
|
||||
// We confirmed and everything's good.
|
||||
resolve({ sent: false })
|
||||
} else {
|
||||
// If after all the tries, just treat it as if user is locked
|
||||
resolve({ sent: false })
|
||||
}
|
||||
.then((sent) => {
|
||||
resolve({ sent })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -285,6 +285,12 @@
|
|||
"search": [
|
||||
"bell-ringing-o"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "0b2b66e526028a6972d51a6f10281b4b",
|
||||
"css": "zoom-in",
|
||||
"code": 59420,
|
||||
"src": "fontawesome"
|
||||
}
|
||||
]
|
||||
}
|
1
static/font/css/fontello-codes.css
vendored
1
static/font/css/fontello-codes.css
vendored
|
@ -27,6 +27,7 @@
|
|||
.icon-pin:before { content: '\e819'; } /* '' */
|
||||
.icon-wrench:before { content: '\e81a'; } /* '' */
|
||||
.icon-chart-bar:before { content: '\e81b'; } /* '' */
|
||||
.icon-zoom-in:before { content: '\e81c'; } /* '' */
|
||||
.icon-spin3:before { content: '\e832'; } /* '' */
|
||||
.icon-spin4:before { content: '\e834'; } /* '' */
|
||||
.icon-link-ext:before { content: '\f08e'; } /* '' */
|
||||
|
|
13
static/font/css/fontello-embedded.css
vendored
13
static/font/css/fontello-embedded.css
vendored
File diff suppressed because one or more lines are too long
1
static/font/css/fontello-ie7-codes.css
vendored
1
static/font/css/fontello-ie7-codes.css
vendored
|
@ -27,6 +27,7 @@
|
|||
.icon-pin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-zoom-in { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
1
static/font/css/fontello-ie7.css
vendored
1
static/font/css/fontello-ie7.css
vendored
|
@ -38,6 +38,7 @@
|
|||
.icon-pin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-zoom-in { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
15
static/font/css/fontello.css
vendored
15
static/font/css/fontello.css
vendored
|
@ -1,11 +1,11 @@
|
|||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('../font/fontello.eot?91349539');
|
||||
src: url('../font/fontello.eot?91349539#iefix') format('embedded-opentype'),
|
||||
url('../font/fontello.woff2?91349539') format('woff2'),
|
||||
url('../font/fontello.woff?91349539') format('woff'),
|
||||
url('../font/fontello.ttf?91349539') format('truetype'),
|
||||
url('../font/fontello.svg?91349539#fontello') format('svg');
|
||||
src: url('../font/fontello.eot?4060331');
|
||||
src: url('../font/fontello.eot?4060331#iefix') format('embedded-opentype'),
|
||||
url('../font/fontello.woff2?4060331') format('woff2'),
|
||||
url('../font/fontello.woff?4060331') format('woff'),
|
||||
url('../font/fontello.ttf?4060331') format('truetype'),
|
||||
url('../font/fontello.svg?4060331#fontello') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
|||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('../font/fontello.svg?91349539#fontello') format('svg');
|
||||
src: url('../font/fontello.svg?4060331#fontello') format('svg');
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
@ -83,6 +83,7 @@
|
|||
.icon-pin:before { content: '\e819'; } /* '' */
|
||||
.icon-wrench:before { content: '\e81a'; } /* '' */
|
||||
.icon-chart-bar:before { content: '\e81b'; } /* '' */
|
||||
.icon-zoom-in:before { content: '\e81c'; } /* '' */
|
||||
.icon-spin3:before { content: '\e832'; } /* '' */
|
||||
.icon-spin4:before { content: '\e834'; } /* '' */
|
||||
.icon-link-ext:before { content: '\f08e'; } /* '' */
|
||||
|
|
|
@ -229,11 +229,11 @@ body {
|
|||
}
|
||||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('./font/fontello.eot?82370835');
|
||||
src: url('./font/fontello.eot?82370835#iefix') format('embedded-opentype'),
|
||||
url('./font/fontello.woff?82370835') format('woff'),
|
||||
url('./font/fontello.ttf?82370835') format('truetype'),
|
||||
url('./font/fontello.svg?82370835#fontello') format('svg');
|
||||
src: url('./font/fontello.eot?25455785');
|
||||
src: url('./font/fontello.eot?25455785#iefix') format('embedded-opentype'),
|
||||
url('./font/fontello.woff?25455785') format('woff'),
|
||||
url('./font/fontello.ttf?25455785') format('truetype'),
|
||||
url('./font/fontello.svg?25455785#fontello') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -340,27 +340,30 @@ body {
|
|||
<div class="the-icons span3" title="Code: 0xe81b"><i class="demo-icon icon-chart-bar"></i> <span class="i-name">icon-chart-bar</span><span class="i-code">0xe81b</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xe81c"><i class="demo-icon icon-zoom-in"></i> <span class="i-name">icon-zoom-in</span><span class="i-code">0xe81c</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-spin3 animate-spin"></i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-spin4 animate-spin"></i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf08e"><i class="demo-icon icon-link-ext"></i> <span class="i-name">icon-link-ext</span><span class="i-code">0xf08e</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt"></i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt"></i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf0c9"><i class="demo-icon icon-menu"></i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf0e0"><i class="demo-icon icon-mail-alt"></i> <span class="i-name">icon-mail-alt</span><span class="i-code">0xf0e0</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf0e5"><i class="demo-icon icon-comment-empty"></i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xf0e5</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf0f3"><i class="demo-icon icon-bell-alt"></i> <span class="i-name">icon-bell-alt</span><span class="i-code">0xf0f3</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xf0f3"><i class="demo-icon icon-bell-alt"></i> <span class="i-name">icon-bell-alt</span><span class="i-code">0xf0f3</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared"></i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf112"><i class="demo-icon icon-reply"></i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf13e"><i class="demo-icon icon-lock-open-alt"></i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xf13e</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf141"><i class="demo-icon icon-ellipsis"></i> <span class="i-name">icon-ellipsis</span><span class="i-code">0xf141</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xf141"><i class="demo-icon icon-ellipsis"></i> <span class="i-name">icon-ellipsis</span><span class="i-code">0xf141</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf144"><i class="demo-icon icon-play-circled"></i> <span class="i-name">icon-play-circled</span><span class="i-code">0xf144</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf164"><i class="demo-icon icon-thumbs-up-alt"></i> <span class="i-name">icon-thumbs-up-alt</span><span class="i-code">0xf164</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars"></i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xf234"><i class="demo-icon icon-user-plus"></i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Binary file not shown.
|
@ -62,6 +62,8 @@
|
|||
|
||||
<glyph glyph-name="chart-bar" unicode="" d="M357 357v-286h-143v286h143z m214 286v-572h-142v572h142z m572-643v-72h-1143v858h71v-786h1072z m-357 500v-429h-143v429h143z m214 214v-643h-143v643h143z" horiz-adv-x="1142.9" />
|
||||
|
||||
<glyph glyph-name="zoom-in" unicode="" d="M571 411v-36q0-7-5-13t-12-5h-125v-125q0-7-6-13t-12-5h-36q-7 0-13 5t-5 13v125h-125q-7 0-12 5t-6 13v36q0 7 6 12t12 5h125v125q0 8 5 13t13 5h36q7 0 12-5t6-13v-125h125q7 0 12-5t5-12z m72-18q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-21-50t-51-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
|
||||
|
||||
<glyph glyph-name="spin3" unicode="" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="spin4" unicode="" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />
|
||||
|
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
27
test/unit/specs/components/timeline.spec.js
Normal file
27
test/unit/specs/components/timeline.spec.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { getExcludedStatusIdsByPinning } from 'src/components/timeline/timeline.js'
|
||||
|
||||
describe('Timeline', () => {
|
||||
describe('getExcludedStatusIdsByPinning', () => {
|
||||
const mockStatuses = (ids) => ids.map(id => ({ id }))
|
||||
|
||||
it('should return only members of both pinnedStatusIds and ids of the given statuses', () => {
|
||||
const statusIds = [1, 2, 3, 4]
|
||||
const statuses = mockStatuses(statusIds)
|
||||
const pinnedStatusIds = [1, 3, 5]
|
||||
const result = getExcludedStatusIdsByPinning(statuses, pinnedStatusIds)
|
||||
result.forEach(item => {
|
||||
expect(item).to.be.oneOf(statusIds)
|
||||
expect(item).to.be.oneOf(pinnedStatusIds)
|
||||
})
|
||||
})
|
||||
|
||||
it('should return ids of pinned statuses not posted before any unpinned status', () => {
|
||||
const pinnedStatusIdSet1 = ['PINNED1', 'PINNED2']
|
||||
const pinnedStatusIdSet2 = ['PINNED3', 'PINNED4']
|
||||
const pinnedStatusIds = [...pinnedStatusIdSet1, ...pinnedStatusIdSet2]
|
||||
const statusIds = [...pinnedStatusIdSet1, 'UNPINNED1', ...pinnedStatusIdSet2]
|
||||
const statuses = mockStatuses(statusIds)
|
||||
expect(getExcludedStatusIdsByPinning(statuses, pinnedStatusIds)).to.eql(pinnedStatusIdSet1)
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Add table
Reference in a new issue