From 2e59ab738b6991a0e6b0a0b9a6dafba41e16c929 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 7 Mar 2019 19:49:41 +0200 Subject: [PATCH 01/37] updates normalizer for proper user handling and adds support for friends tl via mastoapi --- src/services/api/api.service.js | 4 ++-- .../entity_normalizer.service.js | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 2de87026e6..fe2cb1c8b4 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1,6 +1,5 @@ /* eslint-env browser */ const LOGIN_URL = '/api/account/verify_credentials.json' -const FRIENDS_TIMELINE_URL = '/api/statuses/friends_timeline.json' const ALL_FOLLOWING_URL = '/api/qvitter/allfollowing' const PUBLIC_TIMELINE_URL = '/api/statuses/public_timeline.json' const PUBLIC_AND_EXTERNAL_TIMELINE_URL = '/api/statuses/public_and_external_timeline.json' @@ -43,6 +42,7 @@ const DENY_USER_URL = '/api/pleroma/friendships/deny' const SUGGESTIONS_URL = '/api/v1/suggestions' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' +const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home' import { each, map } from 'lodash' import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js' @@ -342,7 +342,7 @@ const setUserMute = ({id, credentials, muted = true}) => { const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false}) => { const timelineUrls = { public: PUBLIC_TIMELINE_URL, - friends: FRIENDS_TIMELINE_URL, + friends: MASTODON_USER_HOME_TIMELINE_URL, mentions: MENTIONS_URL, dms: DM_TIMELINE_URL, notifications: QVITTER_USER_NOTIFICATIONS_URL, diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index d20ce77f77..70f6d693af 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -60,9 +60,18 @@ export const parseUser = (data) => { if (data.pleroma) { const pleroma = data.pleroma - output.follows_you = pleroma.follows_you - output.statusnet_blocking = pleroma.statusnet_blocking - output.muted = pleroma.muted + const relationship = pleroma.relationship + output.follows_you = relationship.followed_by + output.statusnet_blocking = relationship.blocking + output.muted = relationship.muting + output.following = relationship.following + + // Unused: + // domain_blocking + // endorsed + // muting_notifications + // requested + // showing_reblogs } // Missing, trying to recover From c038d0c12a8cc2af9fd3d3c6f9e338ee0d7c4765 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 7 Mar 2019 20:04:29 +0200 Subject: [PATCH 02/37] undo this change since BE returns empty object for relationship, add in a separate MR --- .../entity_normalizer.service.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 70f6d693af..d20ce77f77 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -60,18 +60,9 @@ export const parseUser = (data) => { if (data.pleroma) { const pleroma = data.pleroma - const relationship = pleroma.relationship - output.follows_you = relationship.followed_by - output.statusnet_blocking = relationship.blocking - output.muted = relationship.muting - output.following = relationship.following - - // Unused: - // domain_blocking - // endorsed - // muting_notifications - // requested - // showing_reblogs + output.follows_you = pleroma.follows_you + output.statusnet_blocking = pleroma.statusnet_blocking + output.muted = pleroma.muted } // Missing, trying to recover From 8522063b2c4a35f0926517a3b75376c81131dd79 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 7 Mar 2019 20:16:35 +0200 Subject: [PATCH 03/37] switch public and TWKN to MastoAPI --- src/services/api/api.service.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 2de87026e6..f9b556c0e7 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -2,8 +2,6 @@ const LOGIN_URL = '/api/account/verify_credentials.json' const FRIENDS_TIMELINE_URL = '/api/statuses/friends_timeline.json' const ALL_FOLLOWING_URL = '/api/qvitter/allfollowing' -const PUBLIC_TIMELINE_URL = '/api/statuses/public_timeline.json' -const PUBLIC_AND_EXTERNAL_TIMELINE_URL = '/api/statuses/public_and_external_timeline.json' const TAG_TIMELINE_URL = '/api/statusnet/tags/timeline' const FAVORITE_URL = '/api/favorites/create' const UNFAVORITE_URL = '/api/favorites/destroy' @@ -43,6 +41,7 @@ const DENY_USER_URL = '/api/pleroma/friendships/deny' const SUGGESTIONS_URL = '/api/v1/suggestions' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' +const MASTODON_PUBLIC_TIMELINE = '/api/v1/timelines/public' import { each, map } from 'lodash' import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js' @@ -341,12 +340,12 @@ const setUserMute = ({id, credentials, muted = true}) => { const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false}) => { const timelineUrls = { - public: PUBLIC_TIMELINE_URL, + public: MASTODON_PUBLIC_TIMELINE, friends: FRIENDS_TIMELINE_URL, mentions: MENTIONS_URL, dms: DM_TIMELINE_URL, notifications: QVITTER_USER_NOTIFICATIONS_URL, - 'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL, + 'publicAndExternal': MASTODON_PUBLIC_TIMELINE, user: QVITTER_USER_TIMELINE_URL, media: QVITTER_USER_TIMELINE_URL, favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, @@ -372,6 +371,12 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use if (timeline === 'media') { params.push(['only_media', 1]) } + if (timeline === 'public') { + params.push(['local', true]) + } + if (timeline === 'public' || timeline === 'publicAndExternal') { + params.push(['only_media', false]) + } params.push(['count', 20]) From 6e2946f35290380cbd9e2147a570469f1be18ab6 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 7 Mar 2019 20:21:07 +0200 Subject: [PATCH 04/37] switch direct messages to mastoapi --- src/services/api/api.service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 2de87026e6..65d0916f2a 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -15,7 +15,6 @@ const STATUS_URL = '/api/statuses/show' const MEDIA_UPLOAD_URL = '/api/statusnet/media/upload' const CONVERSATION_URL = '/api/statusnet/conversation' const MENTIONS_URL = '/api/statuses/mentions.json' -const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json' const FOLLOWERS_URL = '/api/statuses/followers.json' const FRIENDS_URL = '/api/statuses/friends.json' const BLOCKS_URL = '/api/statuses/blocks.json' @@ -43,6 +42,7 @@ const DENY_USER_URL = '/api/pleroma/friendships/deny' const SUGGESTIONS_URL = '/api/v1/suggestions' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' +const MASTODON_DIRECT_MESSAGES_TIMELINE_URL = '/api/v1/timelines/direct' import { each, map } from 'lodash' import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js' @@ -344,7 +344,7 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use public: PUBLIC_TIMELINE_URL, friends: FRIENDS_TIMELINE_URL, mentions: MENTIONS_URL, - dms: DM_TIMELINE_URL, + dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL, notifications: QVITTER_USER_NOTIFICATIONS_URL, 'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL, user: QVITTER_USER_TIMELINE_URL, From 0b1506a4c00c1482dd31ee43b17ee673e9f57bde Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 10 Mar 2019 18:05:51 +0200 Subject: [PATCH 05/37] wip support for follower/following, a bit broken and with regression --- src/services/api/api.service.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 2de87026e6..9cee75b9ad 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -16,8 +16,6 @@ const MEDIA_UPLOAD_URL = '/api/statusnet/media/upload' const CONVERSATION_URL = '/api/statusnet/conversation' const MENTIONS_URL = '/api/statuses/mentions.json' const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json' -const FOLLOWERS_URL = '/api/statuses/followers.json' -const FRIENDS_URL = '/api/statuses/friends.json' const BLOCKS_URL = '/api/statuses/blocks.json' const FOLLOWING_URL = '/api/friendships/create.json' const UNFOLLOWING_URL = '/api/friendships/destroy.json' @@ -43,6 +41,8 @@ const DENY_USER_URL = '/api/pleroma/friendships/deny' const SUGGESTIONS_URL = '/api/v1/suggestions' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' +const MASTODON_FOLLOWING_URL = id => `/api/v1/accounts/${id}/following` +const MASTODON_FOLLOWERS_URL = id => `/api/v1/accounts/${id}/followers` import { each, map } from 'lodash' import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js' @@ -258,9 +258,9 @@ const fetchUser = ({id, credentials}) => { } const fetchFriends = ({id, page, credentials}) => { - let url = `${FRIENDS_URL}?user_id=${id}` + let url = MASTODON_FOLLOWING_URL(id) if (page) { - url = url + `&page=${page}` + url = url + `?page=${page}` } return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) @@ -268,16 +268,16 @@ const fetchFriends = ({id, page, credentials}) => { } const exportFriends = ({id, credentials}) => { - let url = `${FRIENDS_URL}?user_id=${id}&all=true` + let url = MASTODON_FOLLOWING_URL(id) + `?all=true` return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) .then((data) => data.map(parseUser)) } const fetchFollowers = ({id, page, credentials}) => { - let url = `${FOLLOWERS_URL}?user_id=${id}` + let url = MASTODON_FOLLOWERS_URL(id) if (page) { - url = url + `&page=${page}` + url = url + `?page=${page}` } return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) From 4a5aef8883d3e92a58e272857cdfb303da61feb2 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 10 Mar 2019 19:15:07 +0200 Subject: [PATCH 06/37] basic user and status actions implemented --- src/modules/statuses.js | 14 ------ src/services/api/api.service.js | 86 +++++++++++++++++++++------------ 2 files changed, 56 insertions(+), 44 deletions(-) diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 7571b62abd..a1ba87c410 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -429,13 +429,6 @@ const statuses = { // Optimistic favoriting... commit('setFavorited', { status, value: true }) apiService.favorite({ id: status.id, credentials: rootState.users.currentUser.credentials }) - .then(response => { - if (response.ok) { - return response.json() - } else { - return {} - } - }) .then(status => { commit('setFavoritedConfirm', { status }) }) @@ -444,13 +437,6 @@ const statuses = { // Optimistic favoriting... commit('setFavorited', { status, value: false }) apiService.unfavorite({ id: status.id, credentials: rootState.users.currentUser.credentials }) - .then(response => { - if (response.ok) { - return response.json() - } else { - return {} - } - }) .then(status => { commit('setFavoritedConfirm', { status }) }) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 2de87026e6..e2f44a2b86 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -5,12 +5,7 @@ const ALL_FOLLOWING_URL = '/api/qvitter/allfollowing' const PUBLIC_TIMELINE_URL = '/api/statuses/public_timeline.json' const PUBLIC_AND_EXTERNAL_TIMELINE_URL = '/api/statuses/public_and_external_timeline.json' const TAG_TIMELINE_URL = '/api/statusnet/tags/timeline' -const FAVORITE_URL = '/api/favorites/create' -const UNFAVORITE_URL = '/api/favorites/destroy' -const RETWEET_URL = '/api/statuses/retweet' -const UNRETWEET_URL = '/api/statuses/unretweet' const STATUS_UPDATE_URL = '/api/statuses/update.json' -const STATUS_DELETE_URL = '/api/statuses/destroy' const STATUS_URL = '/api/statuses/show' const MEDIA_UPLOAD_URL = '/api/statusnet/media/upload' const CONVERSATION_URL = '/api/statusnet/conversation' @@ -19,9 +14,6 @@ const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json' const FOLLOWERS_URL = '/api/statuses/followers.json' const FRIENDS_URL = '/api/statuses/friends.json' const BLOCKS_URL = '/api/statuses/blocks.json' -const FOLLOWING_URL = '/api/friendships/create.json' -const UNFOLLOWING_URL = '/api/friendships/destroy.json' -const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json' const REGISTRATION_URL = '/api/account/register.json' const AVATAR_UPDATE_URL = '/api/qvitter/update_avatar.json' const BG_UPDATE_URL = '/api/qvitter/update_background_image.json' @@ -31,8 +23,6 @@ const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json' const QVITTER_USER_TIMELINE_URL = '/api/qvitter/statuses/user_timeline.json' const QVITTER_USER_NOTIFICATIONS_URL = '/api/qvitter/statuses/notifications.json' const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json' -const BLOCKING_URL = '/api/blocks/create.json' -const UNBLOCKING_URL = '/api/blocks/destroy.json' const USER_URL = '/api/users/show.json' const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import' const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account' @@ -43,6 +33,17 @@ const DENY_USER_URL = '/api/pleroma/friendships/deny' const SUGGESTIONS_URL = '/api/v1/suggestions' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' +const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite` +const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite` +const MASTODON_RETWEET_URL = id => `/api/v1/statuses/${id}/reblog` +const MASTODON_UNRETWEET_URL = id => `/api/v1/statuses/${id}/unreblog` +const MASTODON_DELETE_URL = id => `/api/v1/statuses/${id}` +const MASTODON_FOLLOW_URL = id => `/api/v1/accounts/${id}/follow` +const MASTODON_UNFOLLOW_URL = id => `/api/v1/accounts/${id}/unfollow` +const MASTODON_BLOCK_URL = id => `/api/v1/accounts/${id}/block` +const MASTODON_UNBLOCK_URL = id => `/api/v1/accounts/${id}/unblock` +const MASTODON_MUTE_URL = id => `/api/v1/accounts/${id}/mute` +const MASTODON_UNMUTE_URL = id => `/api/v1/accounts/${id}/unmute` import { each, map } from 'lodash' import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js' @@ -195,7 +196,7 @@ const externalProfile = ({profileUrl, credentials}) => { } const followUser = ({id, credentials}) => { - let url = `${FOLLOWING_URL}?user_id=${id}` + let url = MASTODON_FOLLOW_URL(id) return fetch(url, { headers: authHeaders(credentials), method: 'POST' @@ -203,7 +204,7 @@ const followUser = ({id, credentials}) => { } const unfollowUser = ({id, credentials}) => { - let url = `${UNFOLLOWING_URL}?user_id=${id}` + let url = MASTODON_UNFOLLOW_URL(id) return fetch(url, { headers: authHeaders(credentials), method: 'POST' @@ -211,7 +212,7 @@ const unfollowUser = ({id, credentials}) => { } const blockUser = ({id, credentials}) => { - let url = `${BLOCKING_URL}?user_id=${id}` + let url = MASTODON_BLOCK_URL(id) return fetch(url, { headers: authHeaders(credentials), method: 'POST' @@ -219,7 +220,7 @@ const blockUser = ({id, credentials}) => { } const unblockUser = ({id, credentials}) => { - let url = `${UNBLOCKING_URL}?user_id=${id}` + let url = MASTODON_UNBLOCK_URL(id) return fetch(url, { headers: authHeaders(credentials), method: 'POST' @@ -324,18 +325,11 @@ const fetchStatus = ({id, credentials}) => { } const setUserMute = ({id, credentials, muted = true}) => { - const form = new FormData() + const url = muted ? MASTODON_MUTE_URL(id) : MASTODON_UNMUTE_URL(id) - const muteInteger = muted ? 1 : 0 - - form.append('namespace', 'qvitter') - form.append('data', muteInteger) - form.append('topic', `mute:${id}`) - - return fetch(QVITTER_USER_PREF_URL, { + return fetch(url, { method: 'POST', - headers: authHeaders(credentials), - body: form + headers: authHeaders(credentials) }) } @@ -407,31 +401,63 @@ const verifyCredentials = (user) => { } const favorite = ({ id, credentials }) => { - return fetch(`${FAVORITE_URL}/${id}.json`, { + return fetch(MASTODON_FAVORITE_URL(id), { headers: authHeaders(credentials), method: 'POST' }) + .then(response => { + if (response.ok) { + return response.json() + } else { + return {} + } + }) + .then((data) => parseStatus(data)) } const unfavorite = ({ id, credentials }) => { - return fetch(`${UNFAVORITE_URL}/${id}.json`, { + return fetch(MASTODON_UNFAVORITE_URL(id), { headers: authHeaders(credentials), method: 'POST' }) + .then(response => { + if (response.ok) { + return response.json() + } else { + return {} + } + }) + .then((data) => parseStatus(data)) } const retweet = ({ id, credentials }) => { - return fetch(`${RETWEET_URL}/${id}.json`, { + return fetch(MASTODON_RETWEET_URL(id), { headers: authHeaders(credentials), method: 'POST' }) + .then(response => { + if (response.ok) { + return response.json() + } else { + return {} + } + }) + .then((data) => parseStatus(data)) } const unretweet = ({ id, credentials }) => { - return fetch(`${UNRETWEET_URL}/${id}.json`, { + return fetch(MASTODON_UNRETWEET_URL(id), { headers: authHeaders(credentials), method: 'POST' }) + .then(response => { + if (response.ok) { + return response.json() + } else { + return {} + } + }) + .then((data) => parseStatus(data)) } const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType, noAttachmentLinks}) => { @@ -468,9 +494,9 @@ const postStatus = ({credentials, status, spoilerText, visibility, sensitive, me } const deleteStatus = ({ id, credentials }) => { - return fetch(`${STATUS_DELETE_URL}/${id}.json`, { + return fetch(MASTODON_DELETE_URL(id), { headers: authHeaders(credentials), - method: 'POST' + method: 'DELETE' }) } From 4f455eefe5af2914d7dece40e027ed35ec8a21b3 Mon Sep 17 00:00:00 2001 From: dave Date: Mon, 11 Mar 2019 10:52:28 -0400 Subject: [PATCH 07/37] #433: do not remove the reply dialog --- src/components/conversation/conversation.js | 6 +++++- src/components/conversation/conversation.vue | 15 +++++++++----- src/components/status/status.js | 6 +++--- .../status_or_conversation.js | 6 +++++- .../status_or_conversation.vue | 20 +++++++++++++++++-- 5 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 48b8aaaa51..95e484cda6 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -30,7 +30,8 @@ const conversation = { }, props: [ 'statusoid', - 'collapsable' + 'collapsable', + 'replying' ], computed: { status () { @@ -102,6 +103,9 @@ const conversation = { }, setHighlight (id) { this.highlight = id + }, + toggleReplying () { + this.$emit('toggleReplying') } } } diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 5528fef659..42d009c90d 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -10,14 +10,19 @@
- + class="status-fadein" + />
diff --git a/src/components/status/status.js b/src/components/status/status.js index 9e18fe151c..20ca86a6b7 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -25,11 +25,11 @@ const Status = { 'replies', 'isPreview', 'noHeading', - 'inlineExpanded' + 'inlineExpanded', + 'replying' ], data () { return { - replying: false, expanded: false, unmuted: false, userExpanded: false, @@ -307,7 +307,7 @@ const Status = { } }, toggleReplying () { - this.replying = !this.replying + this.$emit('toggleReplying') }, gotoOriginal (id) { // only handled by conversation, not status_or_conversation diff --git a/src/components/status_or_conversation/status_or_conversation.js b/src/components/status_or_conversation/status_or_conversation.js index 441552ca62..749f76651f 100644 --- a/src/components/status_or_conversation/status_or_conversation.js +++ b/src/components/status_or_conversation/status_or_conversation.js @@ -5,7 +5,8 @@ const statusOrConversation = { props: ['statusoid'], data () { return { - expanded: false + expanded: false, + replying: false } }, components: { @@ -15,6 +16,9 @@ const statusOrConversation = { methods: { toggleExpanded () { this.expanded = !this.expanded + }, + toggleReplying () { + this.replying = !this.replying } } } diff --git a/src/components/status_or_conversation/status_or_conversation.vue b/src/components/status_or_conversation/status_or_conversation.vue index 9647d5eb49..43a60c3a06 100644 --- a/src/components/status_or_conversation/status_or_conversation.vue +++ b/src/components/status_or_conversation/status_or_conversation.vue @@ -1,7 +1,23 @@ From 63d7c7bd80cf8028cdefee99c1cb75614385f96b Mon Sep 17 00:00:00 2001 From: dave Date: Mon, 11 Mar 2019 16:24:37 -0400 Subject: [PATCH 08/37] #433: persistency of status form --- src/components/conversation/conversation.js | 36 +++++++++++++------ src/components/conversation/conversation.vue | 31 +++++++++++----- src/components/status/status.js | 7 ++-- .../status_or_conversation.js | 26 -------------- .../status_or_conversation.vue | 30 ---------------- src/components/timeline/timeline.js | 4 +-- src/components/timeline/timeline.vue | 8 ++++- 7 files changed, 60 insertions(+), 82 deletions(-) delete mode 100644 src/components/status_or_conversation/status_or_conversation.js delete mode 100644 src/components/status_or_conversation/status_or_conversation.vue diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 95e484cda6..4cae0bdb7c 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -1,4 +1,4 @@ -import { reduce, filter } from 'lodash' +import { reduce, filter, findIndex } from 'lodash' import Status from '../status/status.vue' const sortById = (a, b) => { @@ -25,13 +25,13 @@ const sortAndFilterConversation = (conversation) => { const conversation = { data () { return { - highlight: null + highlight: null, + expanded: false } }, props: [ 'statusoid', - 'collapsable', - 'replying' + 'collapsable' ], computed: { status () { @@ -49,9 +49,18 @@ const conversation = { return [] } + if (!this.expanded) { + return [this.status] + } + const conversationId = this.status.statusnet_conversation_id const statuses = this.$store.state.statuses.allStatuses const conversation = filter(statuses, { statusnet_conversation_id: conversationId }) + + const statusIndex = findIndex(conversation, { id: this.statusId }) + if (statusIndex !== -1) { + conversation[statusIndex] = this.status + } return sortAndFilterConversation(conversation) }, replies () { @@ -75,11 +84,13 @@ const conversation = { components: { Status }, - created () { - this.fetchConversation() - }, watch: { - '$route': 'fetchConversation' + '$route': 'fetchConversation', + expanded (value) { + if (value) { + this.fetchConversation() + } + } }, methods: { fetchConversation () { @@ -99,13 +110,16 @@ const conversation = { return this.replies[id] || [] }, focused (id) { - return id === this.statusId + return this.expanded && id === this.statusId }, setHighlight (id) { this.highlight = id }, - toggleReplying () { - this.$emit('toggleReplying') + getHighlight () { + return this.expanded ? this.highlight : null + }, + toggleExpanded () { + this.expanded = !this.expanded } } } diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 42d009c90d..b208d540d0 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -1,9 +1,9 @@ + + diff --git a/src/components/status/status.js b/src/components/status/status.js index 20ca86a6b7..8e489704ff 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -25,11 +25,11 @@ const Status = { 'replies', 'isPreview', 'noHeading', - 'inlineExpanded', - 'replying' + 'inlineExpanded' ], data () { return { + replying: false, expanded: false, unmuted: false, userExpanded: false, @@ -307,10 +307,9 @@ const Status = { } }, toggleReplying () { - this.$emit('toggleReplying') + this.replying = !this.replying }, gotoOriginal (id) { - // only handled by conversation, not status_or_conversation if (this.inConversation) { this.$emit('goto', id) } diff --git a/src/components/status_or_conversation/status_or_conversation.js b/src/components/status_or_conversation/status_or_conversation.js deleted file mode 100644 index 749f76651f..0000000000 --- a/src/components/status_or_conversation/status_or_conversation.js +++ /dev/null @@ -1,26 +0,0 @@ -import Status from '../status/status.vue' -import Conversation from '../conversation/conversation.vue' - -const statusOrConversation = { - props: ['statusoid'], - data () { - return { - expanded: false, - replying: false - } - }, - components: { - Status, - Conversation - }, - methods: { - toggleExpanded () { - this.expanded = !this.expanded - }, - toggleReplying () { - this.replying = !this.replying - } - } -} - -export default statusOrConversation diff --git a/src/components/status_or_conversation/status_or_conversation.vue b/src/components/status_or_conversation/status_or_conversation.vue deleted file mode 100644 index 43a60c3a06..0000000000 --- a/src/components/status_or_conversation/status_or_conversation.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - - - diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index c45f89470d..1da7d5cc52 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -1,6 +1,6 @@ import Status from '../status/status.vue' import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js' -import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue' +import Conversation from '../conversation/conversation.vue' import { throttle } from 'lodash' const Timeline = { @@ -43,7 +43,7 @@ const Timeline = { }, components: { Status, - StatusOrConversation + Conversation }, created () { const store = this.$store diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 8f28d65c78..e0a34bd1b2 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -16,7 +16,13 @@
- +
From 6a9159b25598d578018643673568bbcd06ea1e12 Mon Sep 17 00:00:00 2001 From: dave Date: Mon, 25 Mar 2019 10:50:09 -0400 Subject: [PATCH 09/37] #433 - fix broken conversation page --- src/components/conversation-page/conversation-page.vue | 6 +++++- src/components/conversation/conversation.js | 8 ++++++-- src/components/conversation/conversation.vue | 4 ++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/conversation-page/conversation-page.vue b/src/components/conversation-page/conversation-page.vue index b03eea2822..9e322cf5a5 100644 --- a/src/components/conversation-page/conversation-page.vue +++ b/src/components/conversation-page/conversation-page.vue @@ -1,5 +1,9 @@ diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 4cae0bdb7c..5a9568a9f2 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -31,7 +31,8 @@ const conversation = { }, props: [ 'statusoid', - 'collapsable' + 'collapsable', + 'isPage' ], computed: { status () { @@ -49,7 +50,7 @@ const conversation = { return [] } - if (!this.expanded) { + if (!this.expanded && !this.isPage) { return [this.status] } @@ -79,6 +80,9 @@ const conversation = { i++ return result }, {}) + }, + isExpanded () { + return this.expanded || this.isPage } }, components: { diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index b208d540d0..3778cc1532 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -1,6 +1,6 @@ From 968e6c7fe8392970ddb93751cd8df551ac99c5f1 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 25 Mar 2019 21:04:52 +0200 Subject: [PATCH 13/37] correctly paginate on MastoAPI --- src/modules/users.js | 24 +++++++++---------- src/services/api/api.service.js | 24 ++++++++++++------- .../backend_interactor_service.js | 8 +++---- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/modules/users.js b/src/modules/users.js index 5cfa128ee3..61d0546253 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -52,23 +52,23 @@ export const mutations = { state.loggingIn = false }, // TODO Clean after ourselves? - addFriends (state, { id, friends, page }) { + addFriends (state, { id, friends }) { const user = state.usersObject[id] each(friends, friend => { if (!find(user.friends, { id: friend.id })) { user.friends.push(friend) } }) - user.friendsPage = page + 1 + user.lastFriendId = friends.slice(-1)[0].id }, - addFollowers (state, { id, followers, page }) { + addFollowers (state, { id, followers }) { const user = state.usersObject[id] each(followers, follower => { if (!find(user.followers, { id: follower.id })) { user.followers.push(follower) } }) - user.followersPage = page + 1 + user.lastFollowerId = followers.slice(-1)[0].id }, // Because frontend doesn't have a reason to keep these stuff in memory // outside of viewing someones user profile. @@ -78,7 +78,7 @@ export const mutations = { return } user.friends = [] - user.friendsPage = 0 + user.lastFriendId = null }, clearFollowers (state, userId) { const user = state.usersObject[userId] @@ -86,7 +86,7 @@ export const mutations = { return } user.followers = [] - user.followersPage = 0 + user.lastFollowerId = null }, addNewUsers (state, users) { each(users, (user) => mergeOrAdd(state.users, state.usersObject, user)) @@ -219,10 +219,10 @@ const users = { addFriends ({ rootState, commit }, fetchBy) { return new Promise((resolve, reject) => { const user = rootState.users.usersObject[fetchBy] - const page = user.friendsPage || 1 - rootState.api.backendInteractor.fetchFriends({ id: user.id, page }) + const sinceId = user.lastFriendId + rootState.api.backendInteractor.fetchFriends({ id: user.id, sinceId }) .then((friends) => { - commit('addFriends', { id: user.id, friends, page }) + commit('addFriends', { id: user.id, friends }) resolve(friends) }).catch(() => { reject() @@ -231,10 +231,10 @@ const users = { }, addFollowers ({ rootState, commit }, fetchBy) { const user = rootState.users.usersObject[fetchBy] - const page = user.followersPage || 1 - return rootState.api.backendInteractor.fetchFollowers({ id: user.id, page }) + const sinceId = user.lastFollowerId + return rootState.api.backendInteractor.fetchFollowers({ id: user.id, sinceId }) .then((followers) => { - commit('addFollowers', { id: user.id, followers, page }) + commit('addFollowers', { id: user.id, followers }) return followers }) }, diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 2c5e9d60a1..a8de736c25 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -276,11 +276,15 @@ const fetchUserRelationship = ({id, credentials}) => { }) } -const fetchFriends = ({id, page, credentials}) => { +const fetchFriends = ({id, maxId, sinceId, limit = 20, credentials}) => { let url = MASTODON_FOLLOWING_URL(id) - if (page) { - url = url + `?page=${page}` - } + const args = [ + maxId && `max_id=${maxId}`, + sinceId && `max_id=${sinceId}`, + limit && `limit=${limit}` + ].filter(_ => _).join('&') + + url = url + (args ? '?' + args : '') return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) .then((data) => data.map(parseUser)) @@ -293,11 +297,15 @@ const exportFriends = ({id, credentials}) => { .then((data) => data.map(parseUser)) } -const fetchFollowers = ({id, page, credentials}) => { +const fetchFollowers = ({id, maxId, sinceId, limit = 20, credentials}) => { let url = MASTODON_FOLLOWERS_URL(id) - if (page) { - url = url + `?page=${page}` - } + const args = [ + maxId && `max_id=${maxId}`, + sinceId && `max_id=${sinceId}`, + limit && `limit=${limit}` + ].filter(_ => _).join('&') + + url = url + (args ? '?' + args : '') return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) .then((data) => data.map(parseUser)) diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 0f0bcddc41..71e78d2f0c 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -10,16 +10,16 @@ const backendInteractorService = (credentials) => { return apiService.fetchConversation({id, credentials}) } - const fetchFriends = ({id, page}) => { - return apiService.fetchFriends({id, page, credentials}) + const fetchFriends = ({id, maxId, sinceId, limit}) => { + return apiService.fetchFriends({id, maxId, sinceId, limit, credentials}) } const exportFriends = ({id}) => { return apiService.exportFriends({id, credentials}) } - const fetchFollowers = ({id, page}) => { - return apiService.fetchFollowers({id, page, credentials}) + const fetchFollowers = ({id, maxId, sinceId, limit}) => { + return apiService.fetchFollowers({id, maxId, sinceId, limit, credentials}) } const fetchAllFollowing = ({username}) => { From c5ec4829a7a18b20a1525998ba27cc7af5a3da55 Mon Sep 17 00:00:00 2001 From: dave Date: Mon, 25 Mar 2019 15:10:45 -0400 Subject: [PATCH 14/37] #433 - consider page on focused --- src/components/conversation/conversation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index f88875573d..94709fd86a 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -139,7 +139,7 @@ const conversation = { return this.replies[id] || [] }, focused (id) { - return this.expanded && id === this.statusId + return (this.expanded || this.isPage) && id === this.statusId }, setHighlight (id) { this.highlight = id From 82f077feb944388d0648be1fdace8db1108c5c45 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 25 Mar 2019 21:12:54 +0200 Subject: [PATCH 15/37] fixup! Merge remote-tracking branch 'upstream/develop' into mastoapi/actions --- src/services/api/api.service.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index eac151eb56..70aeb2a951 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -33,10 +33,6 @@ const MASTODON_UNRETWEET_URL = id => `/api/v1/statuses/${id}/unreblog` const MASTODON_DELETE_URL = id => `/api/v1/statuses/${id}` const MASTODON_FOLLOW_URL = id => `/api/v1/accounts/${id}/follow` const MASTODON_UNFOLLOW_URL = id => `/api/v1/accounts/${id}/unfollow` -const MASTODON_BLOCK_URL = id => `/api/v1/accounts/${id}/block` -const MASTODON_UNBLOCK_URL = id => `/api/v1/accounts/${id}/unblock` -const MASTODON_MUTE_URL = id => `/api/v1/accounts/${id}/mute` -const MASTODON_UNMUTE_URL = id => `/api/v1/accounts/${id}/unmute` const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}` const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context` const MASTODON_USER_URL = '/api/v1/accounts' From d766059deef65ea8c46616b340f6038476b47edf Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 25 Mar 2019 21:19:24 +0200 Subject: [PATCH 16/37] correctly update relationship on follow/unfollow --- src/services/follow_manipulate/follow_manipulate.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/follow_manipulate/follow_manipulate.js b/src/services/follow_manipulate/follow_manipulate.js index 1e9bd67916..51dafe84ee 100644 --- a/src/services/follow_manipulate/follow_manipulate.js +++ b/src/services/follow_manipulate/follow_manipulate.js @@ -19,7 +19,7 @@ const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => { export const requestFollow = (user, store) => new Promise((resolve, reject) => { store.state.api.backendInteractor.followUser(user.id) .then((updated) => { - store.commit('addNewUsers', [updated]) + store.commit('updateUserRelationship', [updated]) // For locked users we just mark it that we sent the follow request if (updated.locked) { @@ -66,7 +66,7 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => { export const requestUnfollow = (user, store) => new Promise((resolve, reject) => { store.state.api.backendInteractor.unfollowUser(user.id) .then((updated) => { - store.commit('addNewUsers', [updated]) + store.commit('updateUserRelationship', [updated]) resolve({ updated }) From 467647d5d715730a14d41737e4326696ff971cc2 Mon Sep 17 00:00:00 2001 From: dave Date: Mon, 25 Mar 2019 16:09:40 -0400 Subject: [PATCH 17/37] #433 - fix retweet abnormal behavior --- src/components/conversation/conversation.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 94709fd86a..8ad1f44ddf 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -18,8 +18,13 @@ const sortById = (a, b) => { } } -const sortAndFilterConversation = (conversation) => { - conversation = filter(conversation, (status) => status.type !== 'retweet') +const sortAndFilterConversation = (conversation, statusoid) => { + if (statusoid.type === 'retweet') { + conversation = filter( + conversation, + (status) => (status.type === 'retweet' || status.id !== statusoid.retweeted_status.id) + ) + } return conversation.filter(_ => _).sort(sortById) } @@ -66,7 +71,7 @@ const conversation = { return [] } - if (!this.expanded && !this.isPage) { + if (!this.isExpanded) { return [this.status] } @@ -81,7 +86,7 @@ const conversation = { conversation[statusIndex] = this.status } - return sortAndFilterConversation(conversation) + return sortAndFilterConversation(conversation, this.status) }, replies () { let i = 1 @@ -139,7 +144,7 @@ const conversation = { return this.replies[id] || [] }, focused (id) { - return (this.expanded || this.isPage) && id === this.statusId + return (this.isExpanded) && id === this.status.id }, setHighlight (id) { this.highlight = id @@ -149,6 +154,9 @@ const conversation = { }, toggleExpanded () { this.expanded = !this.expanded + if (!this.expanded) { + this.setHighlight(null) + } } } } From e23c19468a4436454161fd315c6bfb79aef1b15d Mon Sep 17 00:00:00 2001 From: shpuld Date: Mon, 25 Mar 2019 22:44:58 +0200 Subject: [PATCH 18/37] somewhat functioning gesture service --- src/components/side_drawer/side_drawer.js | 21 +++--- .../gesture_service/gesture_service.js | 71 +++++++++++++++++++ 2 files changed, 81 insertions(+), 11 deletions(-) diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index ad3738d167..76c64fe053 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -1,17 +1,18 @@ import UserCard from '../user_card/user_card.vue' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' - -// TODO: separate touch gesture stuff into their own utils if more components want them -const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]] - -const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY]) +import GestureService from '../../services/gesture_service/gesture_service' const SideDrawer = { props: [ 'logout' ], data: () => ({ closed: true, - touchCoord: [0, 0] + closeGesture: undefined }), + created () { + const cb = () => this.toggleDrawer() + this.closeGesture = GestureService.swipeGesture(GestureService.DIRECTION_LEFT, cb) + console.log(this.closeGesture) + }, components: { UserCard }, computed: { currentUser () { @@ -46,13 +47,11 @@ const SideDrawer = { this.toggleDrawer() }, touchStart (e) { - this.touchCoord = touchEventCoord(e) + console.log(this) + GestureService.beginSwipe(e, this.closeGesture) }, touchMove (e) { - const delta = deltaCoord(this.touchCoord, touchEventCoord(e)) - if (delta[0] < -30 && Math.abs(delta[1]) < Math.abs(delta[0]) && !this.closed) { - this.toggleDrawer() - } + GestureService.updateSwipe(e, this.closeGesture) } } } diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js index e69de29bb2..2e39003c8c 100644 --- a/src/services/gesture_service/gesture_service.js +++ b/src/services/gesture_service/gesture_service.js @@ -0,0 +1,71 @@ + +const DIRECTION_LEFT = [-1, 0] +const DIRECTION_RIGHT = [1, 0] +const DIRECTION_UP = [0, -1] +const DIRECTION_DOWN = [0, 1] + +const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]] + +const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY]) + +const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1]) + +const perpendicular = v => [v[1], v[0]] + +const dotProduct = (v1, v2) => v1[0] * v2[0] + v1[1] * v2[1] + +const vectorFlatten = (v1, v2) => [v1[0] * v2[0], v1[1] * v2[1]] + +// direction: either use the constants above or an arbitrary 2d vector. +// threshold: how many Px to move from touch origin before checking if the +// callback should be called. +// divergentTolerance: a scalr for much of divergent direction we tolerate when +// above threshold. for example, with 1.0 we only call the callback if +// divergent component of delta is < 1.0 * direction component of delta. +const swipeGesture = (direction, onSwipe, threshold = 30, perpendicularTolerance = 1.0) => { + return { + direction, + onSwipe, + threshold, + perpendicularTolerance, + _startPos: [0, 0], + _swiping: false + } +} + +const beginSwipe = (event, gesture) => { + gesture._startPos = touchEventCoord(event) + gesture._swiping = true +} + +const updateSwipe = (event, gesture) => { + if (!gesture._swiping) return + // movement too small + const delta = deltaCoord(gesture._startPos, touchEventCoord(event)) + if (vectorLength(delta) < gesture.threshold) return + // movement is opposite from direction + if (dotProduct(delta, gesture.direction) < 0) return + // movement perpendicular to direction is too much + const towardsDir = vectorFlatten(gesture.direction, delta) + const perpendicularDir = perpendicular(gesture.direction) + const towardsPerpendicular = vectorFlatten(perpendicularDir, delta) + if ( + vectorLength(towardsDir) < + gesture.perpendicularTolerance * vectorLength(towardsPerpendicular) + ) return + + gesture.onSwipe() + gesture._swiping = false +} + +const GestureService = { + DIRECTION_LEFT, + DIRECTION_RIGHT, + DIRECTION_UP, + DIRECTION_DOWN, + swipeGesture, + beginSwipe, + updateSwipe +} + +export default GestureService From b97a03383990d54573bd5e68393a1ad11e33608b Mon Sep 17 00:00:00 2001 From: jared Date: Mon, 25 Mar 2019 22:38:15 -0400 Subject: [PATCH 19/37] #255 - add emoji input component --- src/components/emoji-input/emoji-input.js | 106 ++++++++++++++++++ src/components/emoji-input/emoji-input.vue | 99 ++++++++++++++++ .../post_status_form/post_status_form.js | 4 +- .../post_status_form/post_status_form.vue | 5 +- 4 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 src/components/emoji-input/emoji-input.js create mode 100644 src/components/emoji-input/emoji-input.vue diff --git a/src/components/emoji-input/emoji-input.js b/src/components/emoji-input/emoji-input.js new file mode 100644 index 0000000000..56414358a4 --- /dev/null +++ b/src/components/emoji-input/emoji-input.js @@ -0,0 +1,106 @@ +import Completion from '../../services/completion/completion.js' +import { take, filter, map } from 'lodash' + +const EmojiInput = { + props: [ + 'value', + 'placeholder', + 'type' + ], + data () { + return { + highlighted: 0, + caret: 0 + } + }, + computed: { + suggestions () { + const firstchar = this.textAtCaret.charAt(0) + if (firstchar === ':') { + if (this.textAtCaret === ':') { return } + const matchedEmoji = filter(this.emoji.concat(this.customEmoji), (emoji) => emoji.shortcode.startsWith(this.textAtCaret.slice(1))) + if (matchedEmoji.length <= 0) { + return false + } + return map(take(matchedEmoji, 5), ({shortcode, image_url, utf}, index) => ({ + shortcode: `:${shortcode}:`, + utf: utf || '', + // eslint-disable-next-line camelcase + img: utf ? '' : this.$store.state.instance.server + image_url, + highlighted: index === this.highlighted + })) + } else { + return false + } + }, + textAtCaret () { + return (this.wordAtCaret || {}).word || '' + }, + wordAtCaret () { + const word = Completion.wordAtPosition(this.value, this.caret - 1) || {} + return word + }, + emoji () { + return this.$store.state.instance.emoji || [] + }, + customEmoji () { + return this.$store.state.instance.customEmoji || [] + } + }, + methods: { + replace (replacement) { + const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement) + this.$emit('input', newValue) + this.caret = 0 + }, + replaceEmoji (e) { + const len = this.suggestions.length || 0 + if (this.textAtCaret === ':' || e.ctrlKey) { return } + if (len > 0) { + e.preventDefault() + const emoji = this.suggestions[this.highlighted] + const replacement = emoji.utf || (emoji.shortcode + ' ') + const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement) + this.$emit('input', newValue) + this.caret = 0 + this.highlighted = 0 + } + }, + cycleBackward (e) { + const len = this.suggestions.length || 0 + if (len > 0) { + e.preventDefault() + this.highlighted -= 1 + if (this.highlighted < 0) { + this.highlighted = this.suggestions.length - 1 + } + } else { + this.highlighted = 0 + } + }, + cycleForward (e) { + const len = this.suggestions.length || 0 + if (len > 0) { + if (e.shiftKey) { return } + e.preventDefault() + this.highlighted += 1 + if (this.highlighted >= len) { + this.highlighted = 0 + } + } else { + this.highlighted = 0 + } + }, + onKeydown (e) { + e.stopPropagation() + }, + onInput (e) { + this.$emit('input', e.target.value) + }, + setCaret ({target: {selectionStart}}) { + this.caret = selectionStart + } + } +} + +export default EmojiInput diff --git a/src/components/emoji-input/emoji-input.vue b/src/components/emoji-input/emoji-input.vue new file mode 100644 index 0000000000..956063055e --- /dev/null +++ b/src/components/emoji-input/emoji-input.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index c5f30ca643..229aefb744 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -1,5 +1,6 @@ import statusPoster from '../../services/status_poster/status_poster.service.js' import MediaUpload from '../media_upload/media_upload.vue' +import EmojiInput from '../emoji-input/emoji-input.vue' import fileTypeService from '../../services/file_type/file_type.service.js' import Completion from '../../services/completion/completion.js' import { take, filter, reject, map, uniqBy } from 'lodash' @@ -28,7 +29,8 @@ const PostStatusForm = { 'subject' ], components: { - MediaUpload + MediaUpload, + EmojiInput }, mounted () { this.resize(this.$refs.textarea) diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 612f87c1e4..9d449b749a 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -10,12 +10,13 @@ {{ $t('post_status.account_not_locked_warning_link') }}

{{ $t('post_status.direct_warning') }}

- + class="form-cw" + />
+

From 6dc2cedab07d14ef796640644c8b7ce6ec480665 Mon Sep 17 00:00:00 2001 From: jared Date: Tue, 26 Mar 2019 13:42:27 -0400 Subject: [PATCH 22/37] #255 - clean up user settings page with self-closing html tags --- src/components/user_settings/user_settings.vue | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index 27c2f47dde..52df143c24 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -70,7 +70,7 @@

{{$t('settings.avatar')}}

{{$t('settings.avatar_size_instruction')}}

{{$t('settings.current_avatar')}}

- +

{{$t('settings.set_new_avatar')}}

@@ -78,12 +78,11 @@

{{$t('settings.profile_banner')}}

{{$t('settings.current_profile_banner')}}

- +

{{$t('settings.set_new_profile_banner')}}

- - +
- +
@@ -95,10 +94,9 @@

{{$t('settings.profile_background')}}

{{$t('settings.set_new_profile_background')}}

- - +
- +
@@ -174,7 +172,7 @@

{{$t('settings.follow_import')}}

{{$t('settings.import_followers_from_a_csv_file')}}

- +
From 8fe9101f0b134978212bf05ed6e73894f47c617e Mon Sep 17 00:00:00 2001 From: jared Date: Tue, 26 Mar 2019 14:53:27 -0400 Subject: [PATCH 23/37] #255 - clean up autocomplete form --- src/App.scss | 51 +++++++++++++++ src/components/emoji-input/emoji-input.vue | 51 --------------- .../post_status_form/post_status_form.vue | 65 ++++--------------- 3 files changed, 63 insertions(+), 104 deletions(-) diff --git a/src/App.scss b/src/App.scss index 244b34747d..ae068e4fa7 100644 --- a/src/App.scss +++ b/src/App.scss @@ -767,3 +767,54 @@ nav { .btn.btn-default { min-height: 28px; } + +.autocomplete { + &-panel { + position: relative; + + &-body { + margin: 0 0.5em 0 0.5em; + border-radius: $fallback--tooltipRadius; + border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + position: absolute; + z-index: 1; + box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5); + // this doesn't match original but i don't care, making it uniform. + box-shadow: var(--popupShadow); + min-width: 75%; + background: $fallback--bg; + background: var(--bg, $fallback--bg); + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + } + } + + &-item { + cursor: pointer; + padding: 0.2em 0.4em 0.2em 0.4em; + border-bottom: 1px solid rgba(0, 0, 0, 0.4); + display: flex; + + img { + width: 24px; + height: 24px; + object-fit: contain; + } + + span { + line-height: 24px; + margin: 0 0.1em 0 0.2em; + } + + small { + margin-left: .5em; + color: $fallback--faint; + color: var(--faint, $fallback--faint); + } + + &.highlighted { + background-color: $fallback--fg; + background-color: var(--lightBg, $fallback--fg); + } + } +} \ No newline at end of file diff --git a/src/components/emoji-input/emoji-input.vue b/src/components/emoji-input/emoji-input.vue index 568bd080e9..338b77cd06 100644 --- a/src/components/emoji-input/emoji-input.vue +++ b/src/components/emoji-input/emoji-input.vue @@ -61,55 +61,4 @@ width: 100%; } } - -.autocomplete { - &-panel { - position: relative; - - &-body { - margin: 0 0.5em 0 0.5em; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); - position: absolute; - z-index: 1; - box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5); - // this doesn't match original but i don't care, making it uniform. - box-shadow: var(--popupShadow); - min-width: 75%; - background: $fallback--bg; - background: var(--bg, $fallback--bg); - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } - } - - &-item { - cursor: pointer; - padding: 0.2em 0.4em 0.2em 0.4em; - border-bottom: 1px solid rgba(0, 0, 0, 0.4); - display: flex; - - img { - width: 24px; - height: 24px; - object-fit: contain; - } - - span { - line-height: 24px; - margin: 0 0.1em 0 0.2em; - } - - small { - margin-left: .5em; - color: $fallback--faint; - color: var(--faint, $fallback--faint); - } - - &.highlighted { - background-color: $fallback--fg; - background-color: var(--lightBg, $fallback--fg); - } - } -} diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index b2a1dc5831..9f9f16baa3 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -56,14 +56,18 @@
-
-
-
-
- - {{candidate.utf}} - {{candidate.screen_name}}{{candidate.name}} -
+
+
+
+ + {{candidate.utf}} + {{candidate.screen_name}}{{candidate.name}}
@@ -262,50 +266,5 @@ cursor: pointer; z-index: 4; } - - .autocomplete-panel { - margin: 0 0.5em 0 0.5em; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); - position: absolute; - z-index: 1; - box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5); - // this doesn't match original but i don't care, making it uniform. - box-shadow: var(--popupShadow); - min-width: 75%; - background: $fallback--bg; - background: var(--bg, $fallback--bg); - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } - - .autocomplete { - cursor: pointer; - padding: 0.2em 0.4em 0.2em 0.4em; - border-bottom: 1px solid rgba(0, 0, 0, 0.4); - display: flex; - - img { - width: 24px; - height: 24px; - object-fit: contain; - } - - span { - line-height: 24px; - margin: 0 0.1em 0 0.2em; - } - - small { - margin-left: .5em; - color: $fallback--faint; - color: var(--faint, $fallback--faint); - } - - &.highlighted { - background-color: $fallback--fg; - background-color: var(--lightBg, $fallback--fg); - } - } } From c50e64f8eecd780246e3ac47c2a54164cfc28b8f Mon Sep 17 00:00:00 2001 From: shpuld Date: Tue, 26 Mar 2019 22:11:45 +0200 Subject: [PATCH 24/37] Add tests for gesture service, fix bug with perpendicular directions --- .../gesture_service/gesture_service.js | 15 ++- .../gesture_service/gesture_service.spec.js | 120 ++++++++++++++++++ 2 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 test/unit/specs/services/gesture_service/gesture_service.spec.js diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js index 2e39003c8c..efc0ca78f7 100644 --- a/src/services/gesture_service/gesture_service.js +++ b/src/services/gesture_service/gesture_service.js @@ -10,11 +10,14 @@ const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY]) const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1]) -const perpendicular = v => [v[1], v[0]] +const perpendicular = v => [v[1], -v[0]] const dotProduct = (v1, v2) => v1[0] * v2[0] + v1[1] * v2[1] -const vectorFlatten = (v1, v2) => [v1[0] * v2[0], v1[1] * v2[1]] +const project = (v1, v2) => { + const scalar = (dotProduct(v1, v2) / dotProduct(v2, v2)) + return [scalar * v2[0], scalar * v2[1]] +} // direction: either use the constants above or an arbitrary 2d vector. // threshold: how many Px to move from touch origin before checking if the @@ -46,12 +49,12 @@ const updateSwipe = (event, gesture) => { // movement is opposite from direction if (dotProduct(delta, gesture.direction) < 0) return // movement perpendicular to direction is too much - const towardsDir = vectorFlatten(gesture.direction, delta) + const towardsDir = project(delta, gesture.direction) const perpendicularDir = perpendicular(gesture.direction) - const towardsPerpendicular = vectorFlatten(perpendicularDir, delta) + const towardsPerpendicular = project(delta, perpendicularDir) if ( - vectorLength(towardsDir) < - gesture.perpendicularTolerance * vectorLength(towardsPerpendicular) + vectorLength(towardsDir) * gesture.perpendicularTolerance < + vectorLength(towardsPerpendicular) ) return gesture.onSwipe() diff --git a/test/unit/specs/services/gesture_service/gesture_service.spec.js b/test/unit/specs/services/gesture_service/gesture_service.spec.js new file mode 100644 index 0000000000..4a1b009a25 --- /dev/null +++ b/test/unit/specs/services/gesture_service/gesture_service.spec.js @@ -0,0 +1,120 @@ +import GestureService from 'src/services/gesture_service/gesture_service.js' + +const mockTouchEvent = (x, y) => ({ + touches: [ + { + screenX: x, + screenY: y + } + ] +}) + +describe.only('GestureService', () => { + describe('swipeGesture', () => { + it('calls the callback on a successful swipe', () => { + let swiped = false + const callback = () => { swiped = true } + const gesture = GestureService.swipeGesture( + GestureService.DIRECTION_RIGHT, + callback + ) + + GestureService.beginSwipe(mockTouchEvent(100, 100), gesture) + GestureService.updateSwipe(mockTouchEvent(200, 100), gesture) + + expect(swiped).to.eql(true) + }) + + it('calls the callback only once per begin', () => { + let hits = 0 + const callback = () => { hits += 1 } + const gesture = GestureService.swipeGesture( + GestureService.DIRECTION_RIGHT, + callback + ) + + GestureService.beginSwipe(mockTouchEvent(100, 100), gesture) + GestureService.updateSwipe(mockTouchEvent(150, 100), gesture) + GestureService.updateSwipe(mockTouchEvent(200, 100), gesture) + + expect(hits).to.eql(1) + }) + + it('doesn\'t call the callback on an opposite swipe', () => { + let swiped = false + const callback = () => { swiped = true } + const gesture = GestureService.swipeGesture( + GestureService.DIRECTION_RIGHT, + callback + ) + + GestureService.beginSwipe(mockTouchEvent(100, 100), gesture) + GestureService.updateSwipe(mockTouchEvent(0, 100), gesture) + + expect(swiped).to.eql(false) + }) + + it('doesn\'t call the callback on a swipe below threshold', () => { + let swiped = false + const callback = () => { swiped = true } + const gesture = GestureService.swipeGesture( + GestureService.DIRECTION_RIGHT, + callback, + 100 + ) + + GestureService.beginSwipe(mockTouchEvent(100, 100), gesture) + GestureService.updateSwipe(mockTouchEvent(150, 100), gesture) + + expect(swiped).to.eql(false) + }) + + it('doesn\'t call the callback on a perpendicular swipe', () => { + let swiped = false + const callback = () => { swiped = true } + const gesture = GestureService.swipeGesture( + GestureService.DIRECTION_RIGHT, + callback, + 30, + 0.5 + ) + + GestureService.beginSwipe(mockTouchEvent(100, 100), gesture) + GestureService.updateSwipe(mockTouchEvent(150, 200), gesture) + + expect(swiped).to.eql(false) + }) + + it('calls the callback on perpendicular swipe if within tolerance', () => { + let swiped = false + const callback = () => { swiped = true } + const gesture = GestureService.swipeGesture( + GestureService.DIRECTION_RIGHT, + callback, + 30, + 2.0 + ) + + GestureService.beginSwipe(mockTouchEvent(100, 100), gesture) + GestureService.updateSwipe(mockTouchEvent(150, 150), gesture) + + expect(swiped).to.eql(true) + }) + + it('works with any arbitrary 2d directions', () => { + let swiped = false + const callback = () => { swiped = true } + const gesture = GestureService.swipeGesture( + [-1, -1], + callback, + 30, + 0.1 + ) + + GestureService.beginSwipe(mockTouchEvent(100, 100), gesture) + GestureService.updateSwipe(mockTouchEvent(60, 60), gesture) + + expect(swiped).to.eql(true) + }) + }) +}) From 2d05aef24b36d1c8be5cdad13ae69a2f0514c713 Mon Sep 17 00:00:00 2001 From: jasper Date: Tue, 26 Mar 2019 14:26:26 -0700 Subject: [PATCH 25/37] Add await to login action' --- src/boot/after_store.js | 2 +- src/lib/persisted_state.js | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/boot/after_store.js b/src/boot/after_store.js index f5e84cbc67..f5add8ade5 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -241,7 +241,7 @@ const afterStoreSetup = async ({ store, i18n }) => { // Now we have the server settings and can try logging in if (store.state.oauth.token) { - store.dispatch('loginUser', store.state.oauth.token) + await store.dispatch('loginUser', store.state.oauth.token) } const router = new VueRouter({ diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js index 720ff706a3..7ab89c1235 100644 --- a/src/lib/persisted_state.js +++ b/src/lib/persisted_state.js @@ -60,9 +60,6 @@ export default function createPersistedState ({ merge({}, store.state, savedState) ) } - if (store.state.oauth.token) { - store.dispatch('loginUser', store.state.oauth.token) - } loaded = true } catch (e) { console.log("Couldn't load state") From 95459ff0bca7f095f0fdc01b38dd89626fe5c189 Mon Sep 17 00:00:00 2001 From: HJ <30-hj@users.noreply.git.pleroma.social> Date: Wed, 27 Mar 2019 08:47:54 +0000 Subject: [PATCH 26/37] Revert "Merge branch 'mastoapi/friends-tl' into 'develop'" This reverts merge request !647 --- src/services/api/api.service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 4ebfb7b770..3a4f21a69b 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1,5 +1,6 @@ /* eslint-env browser */ const LOGIN_URL = '/api/account/verify_credentials.json' +const FRIENDS_TIMELINE_URL = '/api/statuses/friends_timeline.json' const ALL_FOLLOWING_URL = '/api/qvitter/allfollowing' const PUBLIC_TIMELINE_URL = '/api/statuses/public_timeline.json' const PUBLIC_AND_EXTERNAL_TIMELINE_URL = '/api/statuses/public_and_external_timeline.json' @@ -32,7 +33,6 @@ const DENY_USER_URL = '/api/pleroma/friendships/deny' const SUGGESTIONS_URL = '/api/v1/suggestions' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' -const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home' const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}` const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context` const MASTODON_USER_URL = '/api/v1/accounts' @@ -348,7 +348,7 @@ const fetchStatus = ({id, credentials}) => { const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false, withMuted = false}) => { const timelineUrls = { public: PUBLIC_TIMELINE_URL, - friends: MASTODON_USER_HOME_TIMELINE_URL, + friends: FRIENDS_TIMELINE_URL, mentions: MENTIONS_URL, dms: DM_TIMELINE_URL, notifications: QVITTER_USER_NOTIFICATIONS_URL, From b3a6bec17b18f554caa12d35e9e875adddc206db Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 27 Mar 2019 11:15:57 +0200 Subject: [PATCH 27/37] Revert "Merge branch 'revert-987b5162' into 'develop'" This reverts commit 96753e6a5febff60e0c2cb6fac13d5e7865f0a94, reversing changes made to 987b5162a7530979e9fa887a24311eb1cd480fc5. --- src/services/api/api.service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 8d7f9e5d34..aabbe80f70 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1,6 +1,5 @@ /* eslint-env browser */ const LOGIN_URL = '/api/account/verify_credentials.json' -const FRIENDS_TIMELINE_URL = '/api/statuses/friends_timeline.json' const ALL_FOLLOWING_URL = '/api/qvitter/allfollowing' const TAG_TIMELINE_URL = '/api/statusnet/tags/timeline' const FAVORITE_URL = '/api/favorites/create' @@ -32,6 +31,7 @@ const SUGGESTIONS_URL = '/api/v1/suggestions' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' const MASTODON_PUBLIC_TIMELINE = '/api/v1/timelines/public' +const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home' const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}` const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context` const MASTODON_USER_URL = '/api/v1/accounts' @@ -347,7 +347,7 @@ const fetchStatus = ({id, credentials}) => { const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false, withMuted = false}) => { const timelineUrls = { public: MASTODON_PUBLIC_TIMELINE, - friends: FRIENDS_TIMELINE_URL, + friends: MASTODON_USER_HOME_TIMELINE_URL, mentions: MENTIONS_URL, dms: DM_TIMELINE_URL, notifications: QVITTER_USER_NOTIFICATIONS_URL, From 43e97e590c98d1f1bb500f96d2b604b968fbbbb3 Mon Sep 17 00:00:00 2001 From: dave Date: Wed, 27 Mar 2019 16:00:54 -0400 Subject: [PATCH 28/37] #433 - sort conversation for retweets and clean up --- src/components/conversation/conversation.js | 18 +++++------------- src/components/status/status.vue | 4 ++-- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 8ad1f44ddf..5357b67f77 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -3,19 +3,9 @@ import { set } from 'vue' import Status from '../status/status.vue' const sortById = (a, b) => { - const seqA = Number(a.id) - const seqB = Number(b.id) - const isSeqA = !Number.isNaN(seqA) - const isSeqB = !Number.isNaN(seqB) - if (isSeqA && isSeqB) { - return seqA < seqB ? -1 : 1 - } else if (isSeqA && !isSeqB) { - return -1 - } else if (!isSeqA && isSeqB) { - return 1 - } else { - return a.id < b.id ? -1 : 1 - } + const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id + const idB = b.type === 'retweet' ? b.retweeted_status.id : b.id + return idA < idB ? -1 : 1 } const sortAndFilterConversation = (conversation, statusoid) => { @@ -24,6 +14,8 @@ const sortAndFilterConversation = (conversation, statusoid) => { conversation, (status) => (status.type === 'retweet' || status.id !== statusoid.retweeted_status.id) ) + } else { + conversation = filter(conversation, (status) => status.type !== 'retweet') } return conversation.filter(_ => _).sort(sortById) } diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 1f6d0325ee..da329deb08 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -12,7 +12,7 @@