From 039a4074006fb91ac9031b41b4e9af4a15766dfa Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sat, 12 Jan 2019 23:33:45 +0300 Subject: some initial work to make it possible to use "unregistered" timelines, i.e. not reserving a timeline by name, instead just passing timeline object itself. --- src/services/api/api.service.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src/services/api/api.service.js') diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 4ee95bd1..24b38dfd 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -41,6 +41,8 @@ const APPROVE_USER_URL = '/api/pleroma/friendships/approve' const DENY_USER_URL = '/api/pleroma/friendships/deny' const SUGGESTIONS_URL = '/api/v1/suggestions' +const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' + import { each, map } from 'lodash' import 'whatwg-fetch' @@ -300,10 +302,11 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use notifications: QVITTER_USER_NOTIFICATIONS_URL, 'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL, user: QVITTER_USER_TIMELINE_URL, + favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, tag: TAG_TIMELINE_URL } - let url = timelineUrls[timeline] + let url = timelineUrls[timeline.type || timeline] let params = [] -- cgit v1.2.3-70-g09d2 From 4be737b4df76b4ff730ac7a474ff744d5d42d256 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 13 Jan 2019 01:26:24 +0300 Subject: moved some stuff from statuses to api service. added initial adapter (currently mastoapi status -> qvitter status, would change some naming in the future) favorites timeline works somewhat, notifications are broken because they are fetched using same code to fetch usual timeline/using old architechture --- src/modules/statuses.js | 50 +++------------------ src/services/api/api.service.js | 97 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 44 deletions(-) (limited to 'src/services/api/api.service.js') diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 33804d39..b4d61b13 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -1,4 +1,4 @@ -import { includes, remove, slice, sortBy, toInteger, each, find, flatten, maxBy, minBy, merge, last, isArray } from 'lodash' +import { remove, slice, sortBy, toInteger, each, find, flatten, maxBy, minBy, merge, last, isArray } from 'lodash' import apiService from '../services/api/api.service.js' // import parse from '../services/status_parser/status_parser.js' @@ -43,20 +43,7 @@ export const defaultState = { } } -const isNsfw = (status) => { - const nsfwRegex = /#nsfw/i - return includes(status.tags, 'nsfw') || !!status.text.match(nsfwRegex) -} - export const prepareStatus = (status) => { - // Parse nsfw tags - if (status.nsfw === undefined) { - status.nsfw = isNsfw(status) - if (status.retweeted_status) { - status.nsfw = status.retweeted_status.nsfw - } - } - // Set deleted flag status.deleted = false @@ -75,31 +62,6 @@ const visibleNotificationTypes = (rootState) => { ].filter(_ => _) } -export const statusType = (status) => { - if (status.is_post_verb) { - return 'status' - } - - if (status.retweeted_status) { - return 'retweet' - } - - if ((typeof status.uri === 'string' && status.uri.match(/(fave|objectType=Favourite)/)) || - (typeof status.text === 'string' && status.text.match(/favorited/))) { - return 'favorite' - } - - if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) { - return 'deletion' - } - - if (status.text.match(/started following/) || status.activity_type === 'follow') { - return 'follow' - } - - return 'unknown' -} - export const findMaxId = (...args) => { return (maxBy(flatten(args), 'id') || {}).id } @@ -153,13 +115,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us return } - const addStatus = (status, showImmediately, addToTimeline = true) => { - const result = mergeOrAdd(allStatuses, allStatusesObject, status) - status = result.item + const addStatus = (data, showImmediately, addToTimeline = true) => { + const result = mergeOrAdd(allStatuses, allStatusesObject, data) + const status = result.item if (result.new) { // We are mentioned in a post - if (statusType(status) === 'status' && find(status.attentions, { id: user.id })) { + if (status.type === 'status' && find(status.attentions, { id: user.id })) { const mentions = state.timelines.mentions // Add the mention to the mentions timeline @@ -270,7 +232,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us } each(statuses, (status) => { - const type = statusType(status) + const type = status.type const processor = processors[type] || processors['default'] processor(status) }) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 24b38dfd..ff36a59b 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -72,6 +72,7 @@ const updateAvatar = ({credentials, params}) => { form.append(key, value) } }) + return fetch(url, { headers: authHeaders(credentials), method: 'POST', @@ -89,6 +90,7 @@ const updateBg = ({credentials, params}) => { form.append(key, value) } }) + return fetch(url, { headers: authHeaders(credentials), method: 'POST', @@ -112,6 +114,7 @@ const updateBanner = ({credentials, params}) => { form.append(key, value) } }) + return fetch(url, { headers: authHeaders(credentials), method: 'POST', @@ -293,6 +296,99 @@ const setUserMute = ({id, credentials, muted = true}) => { }) } +export const statusType = (status) => { + if (status.is_post_verb) { + return 'status' + } + + if (status.retweeted_status) { + return 'retweet' + } + + if ((typeof status.uri === 'string' && status.uri.match(/(fave|objectType=Favourite)/)) || + (typeof status.text === 'string' && status.text.match(/favorited/))) { + return 'favorite' + } + + if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) { + return 'deletion' + } + + if (status.text.match(/started following/) || status.activity_type === 'follow') { + return 'follow' + } + + return 'unknown' +} + +const isMastoAPI = (status) => { + return status.hasOwnProperty('account') +} + +const parseUser = (data) => { + return { + id: data.id, + screen_name: data.screen_name || data.acct + } +} + +const parseAttachment = (data) => { + return { + ...data, + mimetype: data.mimetype || data.type + } +} + +const parseData = (data) => { + const output = {} + const masto = isMastoAPI(data) + output.raw = data + output.id = data.id + + output.user = parseUser(masto ? data.account : data.user) + + output.attentions = ((masto ? data.mentions : data.attentions) || []).map(_ => ({ + id: _.id, + following: _.following // FIXME: MastoAPI doesn't have this + })) + + // FIXME: Masto doesn't have "raw text" data, using html data... + output.text = masto ? data.content : data.text + + output.attachments = ((masto ? data.media_attachments : data.attachments) || []).map(parseAttachment) + + const retweetedStatus = masto ? data.reblog : data.retweeted_status + if (retweetedStatus) { + output.retweeted_status = parseData(retweetedStatus) + } + + if (masto) { + output.type = data.reblog ? 'retweet' : 'status' + output.nsfw = data.sensitive + output.statusnet_html = data.content + } else { + // catchall, temporary + Object.assign(output, data) + + // QVitterAPI + output.type = statusType(data) + + if (data.nsfw === undefined) { + output.nsfw = isNsfw(data) + if (data.retweeted_status) { + output.nsfw = data.retweeted_status.nsfw + } + } + } + + return output +} + +const isNsfw = (status) => { + const nsfwRegex = /#nsfw/i + return (status.tags || []).includes('nsfw') || !!status.text.match(nsfwRegex) +} + const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false}) => { const timelineUrls = { public: PUBLIC_TIMELINE_URL, @@ -336,6 +432,7 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use throw new Error('Error fetching timeline') }) .then((data) => data.json()) + .then((data) => data.map(parseData)) } const verifyCredentials = (user) => { -- cgit v1.2.3-70-g09d2 From 519f49e29b38e568727dd7f59618002459a64d64 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 13 Jan 2019 22:07:55 +0300 Subject: separated normalization into a another file, removed catchall and added more stuff --- src/components/conversation/conversation.js | 9 +- src/services/api/api.service.js | 105 ++---------------- .../status_normalizer/status_normalizer.service.js | 118 +++++++++++++++++++++ 3 files changed, 131 insertions(+), 101 deletions(-) create mode 100644 src/services/status_normalizer/status_normalizer.service.js (limited to 'src/services/api/api.service.js') diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 9d9f7bbe..7bad14a5 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -1,9 +1,8 @@ import { reduce, filter, sortBy } from 'lodash' -import { statusType } from '../../modules/statuses.js' import Status from '../status/status.vue' const sortAndFilterConversation = (conversation) => { - conversation = filter(conversation, (status) => statusType(status) !== 'retweet') + conversation = filter(conversation, (status) => status.type !== 'retweet') return sortBy(conversation, 'id') } @@ -18,10 +17,12 @@ const conversation = { 'collapsable' ], computed: { - status () { return this.statusoid }, + status () { + return this.statusoid + }, conversation () { if (!this.status) { - return false + return [] } const conversationId = this.status.statusnet_conversation_id diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index ff36a59b..48e5d480 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -44,6 +44,7 @@ const SUGGESTIONS_URL = '/api/v1/suggestions' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' import { each, map } from 'lodash' +import { parseStatus } from '../status_normalizer/status_normalizer.service.js' import 'whatwg-fetch' const oldfetch = window.fetch @@ -272,12 +273,14 @@ const fetchConversation = ({id, credentials}) => { let url = `${CONVERSATION_URL}/${id}.json?count=100` return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) + .then((data) => data.map(parseStatus)) } const fetchStatus = ({id, credentials}) => { let url = `${STATUS_URL}/${id}.json` return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) + .then((data) => parseStatus(data)) } const setUserMute = ({id, credentials, muted = true}) => { @@ -296,99 +299,6 @@ const setUserMute = ({id, credentials, muted = true}) => { }) } -export const statusType = (status) => { - if (status.is_post_verb) { - return 'status' - } - - if (status.retweeted_status) { - return 'retweet' - } - - if ((typeof status.uri === 'string' && status.uri.match(/(fave|objectType=Favourite)/)) || - (typeof status.text === 'string' && status.text.match(/favorited/))) { - return 'favorite' - } - - if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) { - return 'deletion' - } - - if (status.text.match(/started following/) || status.activity_type === 'follow') { - return 'follow' - } - - return 'unknown' -} - -const isMastoAPI = (status) => { - return status.hasOwnProperty('account') -} - -const parseUser = (data) => { - return { - id: data.id, - screen_name: data.screen_name || data.acct - } -} - -const parseAttachment = (data) => { - return { - ...data, - mimetype: data.mimetype || data.type - } -} - -const parseData = (data) => { - const output = {} - const masto = isMastoAPI(data) - output.raw = data - output.id = data.id - - output.user = parseUser(masto ? data.account : data.user) - - output.attentions = ((masto ? data.mentions : data.attentions) || []).map(_ => ({ - id: _.id, - following: _.following // FIXME: MastoAPI doesn't have this - })) - - // FIXME: Masto doesn't have "raw text" data, using html data... - output.text = masto ? data.content : data.text - - output.attachments = ((masto ? data.media_attachments : data.attachments) || []).map(parseAttachment) - - const retweetedStatus = masto ? data.reblog : data.retweeted_status - if (retweetedStatus) { - output.retweeted_status = parseData(retweetedStatus) - } - - if (masto) { - output.type = data.reblog ? 'retweet' : 'status' - output.nsfw = data.sensitive - output.statusnet_html = data.content - } else { - // catchall, temporary - Object.assign(output, data) - - // QVitterAPI - output.type = statusType(data) - - if (data.nsfw === undefined) { - output.nsfw = isNsfw(data) - if (data.retweeted_status) { - output.nsfw = data.retweeted_status.nsfw - } - } - } - - return output -} - -const isNsfw = (status) => { - const nsfwRegex = /#nsfw/i - return (status.tags || []).includes('nsfw') || !!status.text.match(nsfwRegex) -} - const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false}) => { const timelineUrls = { public: PUBLIC_TIMELINE_URL, @@ -401,10 +311,11 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, tag: TAG_TIMELINE_URL } + const type = timeline.type || timeline + const isNotifications = type === 'notifications' + const params = [] - let url = timelineUrls[timeline.type || timeline] - - let params = [] + let url = timelineUrls[type] if (since) { params.push(['since_id', since]) @@ -432,7 +343,7 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use throw new Error('Error fetching timeline') }) .then((data) => data.json()) - .then((data) => data.map(parseData)) + .then((data) => data.map(isNotifications ? _ => _ : parseStatus)) } const verifyCredentials = (user) => { diff --git a/src/services/status_normalizer/status_normalizer.service.js b/src/services/status_normalizer/status_normalizer.service.js new file mode 100644 index 00000000..ce7cd050 --- /dev/null +++ b/src/services/status_normalizer/status_normalizer.service.js @@ -0,0 +1,118 @@ +export const qvitterStatusType = (status) => { + if (status.is_post_verb) { + return 'status' + } + + if (status.retweeted_status) { + return 'retweet' + } + + if ((typeof status.uri === 'string' && status.uri.match(/(fave|objectType=Favourite)/)) || + (typeof status.text === 'string' && status.text.match(/favorited/))) { + return 'favorite' + } + + if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) { + return 'deletion' + } + + if (status.text.match(/started following/) || status.activity_type === 'follow') { + return 'follow' + } + + return 'unknown' +} + +const isMastoAPI = (status) => { + return status.hasOwnProperty('account') +} + +const parseUser = (data) => { + return { + id: data.id, + screen_name: data.screen_name || data.acct + } +} + +const parseAttachment = (data) => { + return { + ...data, + mimetype: data.mimetype || data.type + } +} + +export const parseStatus = (data) => { + const output = {} + const masto = isMastoAPI(data) + output.raw = data + + console.log(masto ? 'MAMMAL' : 'OLD SHIT') + console.log(data) + if (masto) { + output.favorited = data.favourited + output.fave_num = data.favourites_count + + output.repeated = data.reblogged + output.repeat_num = data.reblogs_count + + output.type = data.reblog ? 'retweet' : 'status' + output.nsfw = data.sensitive + + output.statusnet_html = data.content + // Not exactly the same... + output.text = data.content + + output.in_reply_to_status_id = data.in_reply_to_id + output.in_reply_to_user_id = data.in_reply_to_user_id + } else { + output.favorited = data.favorited + output.fave_num = data.fave_num + + output.repeated = data.repeated + output.repeat_num = data.repeat_num + + // catchall, temporary + // Object.assign(output, data) + + output.type = qvitterStatusType(data) + + if (data.nsfw === undefined) { + output.nsfw = isNsfw(data) + if (data.retweeted_status) { + output.nsfw = data.retweeted_status.nsfw + } + } + output.statusnet_html = data.statusnet_html + output.text = data.text + + output.in_reply_to_status_id = data.in_reply_to_id + output.in_reply_to_user_id = data.in_reply_to_account_id + } + + output.id = Number(data.id) + output.visibility = data.visibility + output.created_at = new Date(data.created_at) + + output.user = parseUser(masto ? data.account : data.user) + + output.attentions = ((masto ? data.mentions : data.attentions) || []) + .map(_ => ({ + id: _.id, + following: _.following // FIXME: MastoAPI doesn't have this + })) + + output.attachments = ((masto ? data.media_attachments : data.attachments) || []) + .map(parseAttachment) + + const retweetedStatus = masto ? data.reblog : data.retweeted_status + if (retweetedStatus) { + output.retweeted_status = parseStatus(retweetedStatus) + } + + return output +} + +const isNsfw = (status) => { + const nsfwRegex = /#nsfw/i + return (status.tags || []).includes('nsfw') || !!status.text.match(nsfwRegex) +} -- cgit v1.2.3-70-g09d2 From e0fd6d12ed8e4105a5c95bf4f5b36bec4bd31183 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 14 Jan 2019 15:30:14 +0300 Subject: user normalizer works --- src/services/api/api.service.js | 8 ++- .../status_normalizer/status_normalizer.service.js | 70 ++++++++++++++++++++-- 2 files changed, 71 insertions(+), 7 deletions(-) (limited to 'src/services/api/api.service.js') diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 48e5d480..de72bdbb 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -44,7 +44,7 @@ const SUGGESTIONS_URL = '/api/v1/suggestions' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' import { each, map } from 'lodash' -import { parseStatus } from '../status_normalizer/status_normalizer.service.js' +import { parseStatus, parseUser } from '../status_normalizer/status_normalizer.service.js' import 'whatwg-fetch' const oldfetch = window.fetch @@ -243,24 +243,28 @@ const fetchUser = ({id, credentials}) => { let url = `${USER_URL}?user_id=${id}` return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) + .then((data) => parseUser(data)) } const fetchFriends = ({id, credentials}) => { let url = `${FRIENDS_URL}?user_id=${id}` return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) + .then((data) => data.map(parseUser)) } const fetchFollowers = ({id, credentials}) => { let url = `${FOLLOWERS_URL}?user_id=${id}` return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) + .then((data) => data.map(parseUser)) } const fetchAllFollowing = ({username, credentials}) => { const url = `${ALL_FOLLOWING_URL}/${username}.json` return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) + .then((data) => data.map(parseUser)) } const fetchFollowRequests = ({credentials}) => { @@ -280,7 +284,7 @@ const fetchStatus = ({id, credentials}) => { let url = `${STATUS_URL}/${id}.json` return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) - .then((data) => parseStatus(data)) + .then((data) => data.map(parseStatus)) } const setUserMute = ({id, credentials, muted = true}) => { diff --git a/src/services/status_normalizer/status_normalizer.service.js b/src/services/status_normalizer/status_normalizer.service.js index ce7cd050..6a575bf5 100644 --- a/src/services/status_normalizer/status_normalizer.service.js +++ b/src/services/status_normalizer/status_normalizer.service.js @@ -1,4 +1,4 @@ -export const qvitterStatusType = (status) => { +const qvitterStatusType = (status) => { if (status.is_post_verb) { return 'status' } @@ -27,11 +27,71 @@ const isMastoAPI = (status) => { return status.hasOwnProperty('account') } -const parseUser = (data) => { - return { - id: data.id, - screen_name: data.screen_name || data.acct +export const parseUser = (data) => { + const output = {} + const masto = data.hasOwnProperty('acct') + // case for users in "mentions" property for statuses in MastoAPI + const mastoShort = masto && !data.hasOwnProperty('avatar') + + output.id = data.id + + if (masto) { + output.screen_name = data.acct + + // There's nothing else to get + if (mastoShort) { + return output + } + + output.name = null // missing + output.name_html = data.display_name + + output.description = null // missing + output.description_html = data.note + + // Utilize avatar_static for gif avatars? + output.profile_image_url = data.avatar + output.profile_image_url_original = data.avatar + + // Same, utilize header_static? + output.cover_photo = data.header + + output.friends_count = data.following_count + + output.bot = data.bot + + output.statusnet_profile_url = data.url + + // Missing, trying to recover + output.is_local = !output.screen_name.includes('@') + } else { + output.screen_name = data.screen_name + + output.name = data.name + output.name_html = data.name_html + + output.description = data.description + output.description_html = data.description_html + + output.profile_image_url = data.profile_image_url + output.profile_image_url_original = data.profile_image_url_original + + output.cover_photo = data.cover_photo + + output.friends_count = data.friends_count + + output.bot = null // missing + + output.statusnet_profile_url = data.statusnet_profile_url + output.is_local = data.is_local } + + output.created_at = new Date(data.created_at) + output.locked = data.locked + output.followers_count = data.followers_count + output.statuses_count = data.statuses_count + + return output } const parseAttachment = (data) => { -- cgit v1.2.3-70-g09d2 From 790fcf37d223f129640aca20c7e185a26b226cdd Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 14 Jan 2019 22:38:37 +0300 Subject: notifications now also undergo some parsing, hypothetically could use MastoAPI notifications, maybe. --- src/modules/statuses.js | 37 ++++++++------------ src/modules/users.js | 5 +-- src/services/api/api.service.js | 4 +-- .../status_normalizer/status_normalizer.service.js | 39 +++++++++++++++++++--- 4 files changed, 53 insertions(+), 32 deletions(-) (limited to 'src/services/api/api.service.js') diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 3118e686..88791191 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -250,42 +250,33 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot const allStatuses = state.allStatuses const allStatusesObject = state.allStatusesObject each(notifications, (notification) => { - const result = mergeOrAdd(allStatuses, allStatusesObject, notification.notice) - const action = result.item + notification.action = mergeOrAdd(allStatuses, allStatusesObject, notification.action).item + notification.status = notification.status && mergeOrAdd(allStatuses, allStatusesObject, notification.status).item + // Only add a new notification if we don't have one for the same action - if (!find(state.notifications.data, (oldNotification) => oldNotification.action.id === action.id)) { + if (!state.notifications.idStore.hasOwnProperty(notification.id)) { state.notifications.maxId = Math.max(notification.id, state.notifications.maxId) state.notifications.minId = Math.min(notification.id, state.notifications.minId) - const fresh = !notification.is_seen - const status = notification.ntype === 'like' - ? action.favorited_status - : action - - const result = { - type: notification.ntype, - status, - action, - seen: !fresh - } - - state.notifications.data.push(result) - state.notifications.idStore[notification.id] = result + console.log('AWOOOOOO', notification) + state.notifications.data.push(notification) + state.notifications.idStore[notification.id] = notification if ('Notification' in window && window.Notification.permission === 'granted') { + const notifObj = {} + const action = notification.action const title = action.user.name - const result = {} - result.icon = action.user.profile_image_url - result.body = action.text // there's a problem that it doesn't put a space before links tho + notifObj.icon = action.user.profile_image_url + notifObj.body = action.text // there's a problem that it doesn't put a space before links tho // Shows first attached non-nsfw image, if any. Should add configuration for this somehow... if (action.attachments && action.attachments.length > 0 && !action.nsfw && action.attachments[0].mimetype.startsWith('image/')) { - result.image = action.attachments[0].url + notifObj.image = action.attachments[0].url } - if (fresh && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.ntype)) { - let notification = new window.Notification(title, result) + if (notification.fresh && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.ntype)) { + let notification = new window.Notification(title, notifObj) // Chrome is known for not closing notifications automatically // according to MDN, anyway. setTimeout(notification.close.bind(notification), 5000) diff --git a/src/modules/users.js b/src/modules/users.js index adbd37dd..33c02a07 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -68,6 +68,7 @@ export const mutations = { }, setUserForNotification (state, notification) { notification.action.user = state.usersObject[notification.action.user.id] + notification.from_profile = state.usersObject[notification.action.user.id] }, setColor (state, { user: { id }, highlighted }) { const user = state.usersObject[id] @@ -149,8 +150,8 @@ const users = { }) }, addNewNotifications (store, { notifications }) { - const users = compact(map(notifications, 'from_profile')) - const notificationIds = compact(notifications.map(_ => String(_.id))) + const users = map(notifications, 'from_profile') + const notificationIds = notifications.map(_ => String(_.id)) store.commit('addNewUsers', users) const notificationsObject = store.rootState.statuses.notifications.idStore diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index de72bdbb..b6180403 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -44,7 +44,7 @@ const SUGGESTIONS_URL = '/api/v1/suggestions' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' import { each, map } from 'lodash' -import { parseStatus, parseUser } from '../status_normalizer/status_normalizer.service.js' +import { parseStatus, parseUser, parseNotification } from '../status_normalizer/status_normalizer.service.js' import 'whatwg-fetch' const oldfetch = window.fetch @@ -347,7 +347,7 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use throw new Error('Error fetching timeline') }) .then((data) => data.json()) - .then((data) => data.map(isNotifications ? _ => _ : parseStatus)) + .then((data) => data.map(isNotifications ? parseNotification : parseStatus)) } const verifyCredentials = (user) => { diff --git a/src/services/status_normalizer/status_normalizer.service.js b/src/services/status_normalizer/status_normalizer.service.js index 6a575bf5..0dbceaa4 100644 --- a/src/services/status_normalizer/status_normalizer.service.js +++ b/src/services/status_normalizer/status_normalizer.service.js @@ -23,10 +23,6 @@ const qvitterStatusType = (status) => { return 'unknown' } -const isMastoAPI = (status) => { - return status.hasOwnProperty('account') -} - export const parseUser = (data) => { const output = {} const masto = data.hasOwnProperty('acct') @@ -95,6 +91,7 @@ export const parseUser = (data) => { } const parseAttachment = (data) => { + // TODO A little bit messy ATM but works with both APIs return { ...data, mimetype: data.mimetype || data.type @@ -103,11 +100,12 @@ const parseAttachment = (data) => { export const parseStatus = (data) => { const output = {} - const masto = isMastoAPI(data) + const masto = data.hasOwnProperty('account') output.raw = data console.log(masto ? 'MAMMAL' : 'OLD SHIT') console.log(data) + if (masto) { output.favorited = data.favourited output.fave_num = data.favourites_count @@ -172,6 +170,37 @@ export const parseStatus = (data) => { return output } +export const parseNotification = (data) => { + const mastoDict = { + 'favourite': 'like', + 'reblog': 'repeat' + } + const masto = !data.hasOwnProperty('ntype') + const output = {} + + if (masto) { + output.type = mastoDict[data.type] || data.type + output.seen = null // missing + output.status = parseStatus(data.status) + output.action = null // missing + output.from_profile = parseUser(data.account) + } else { + const parsedNotice = parseStatus(data.notice) + output.type = data.ntype + output.seen = data.is_seen + output.status = output.type === 'like' + ? parseStatus(data.notice.favorited_status) + : parsedNotice + output.action = parsedNotice + output.from_profile = parseUser(data.from_profile) + } + + output.created_at = new Date(data.created_at) + output.id = data.id + + return output +} + const isNsfw = (status) => { const nsfwRegex = /#nsfw/i return (status.tags || []).includes('nsfw') || !!status.text.match(nsfwRegex) -- cgit v1.2.3-70-g09d2 From fd27c78c4b85ff889833bd247ab0cfc53ad7b176 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 14 Jan 2019 22:58:23 +0300 Subject: fixes, guards --- src/modules/statuses.js | 2 +- src/services/api/api.service.js | 14 +++++++++++++- .../status_normalizer/status_normalizer.service.js | 8 +++++++- 3 files changed, 21 insertions(+), 3 deletions(-) (limited to 'src/services/api/api.service.js') diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 88791191..73d034f1 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -300,7 +300,7 @@ export const mutations = { }, clearTimeline (state, { timeline }) { const timelineObject = typeof timeline === 'object' ? timeline : state.timelines[timeline] - emptyTl(timelineObject, state.timelines[timeline].userId) + emptyTl(timelineObject, timeline.userId) }, setFavorited (state, { status, value }) { const newStatus = state.allStatusesObject[status.id] diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index b6180403..e82f4f81 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -277,6 +277,12 @@ const fetchConversation = ({id, credentials}) => { let url = `${CONVERSATION_URL}/${id}.json?count=100` return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) + .then((data) => { + if (data.ok) { + return data + } + throw new Error('Error fetching timeline') + }) .then((data) => data.map(parseStatus)) } @@ -284,7 +290,13 @@ const fetchStatus = ({id, credentials}) => { let url = `${STATUS_URL}/${id}.json` return fetch(url, { headers: authHeaders(credentials) }) .then((data) => data.json()) - .then((data) => data.map(parseStatus)) + .then((data) => { + if (data.ok) { + return data + } + throw new Error('Error fetching timeline') + }) + .then((data) => parseStatus(data)) } const setUserMute = ({id, credentials, muted = true}) => { diff --git a/src/services/status_normalizer/status_normalizer.service.js b/src/services/status_normalizer/status_normalizer.service.js index 0dbceaa4..5ca5a279 100644 --- a/src/services/status_normalizer/status_normalizer.service.js +++ b/src/services/status_normalizer/status_normalizer.service.js @@ -117,11 +117,15 @@ export const parseStatus = (data) => { output.nsfw = data.sensitive output.statusnet_html = data.content - // Not exactly the same... + + // Not exactly the same but works? output.text = data.content output.in_reply_to_status_id = data.in_reply_to_id output.in_reply_to_user_id = data.in_reply_to_user_id + + // Not exactly the same but works + output.statusnet_conversation_id = data.id } else { output.favorited = data.favorited output.fave_num = data.fave_num @@ -145,6 +149,8 @@ export const parseStatus = (data) => { output.in_reply_to_status_id = data.in_reply_to_id output.in_reply_to_user_id = data.in_reply_to_account_id + + output.statusnet_conversation_id = data.statusnet_conversation_id } output.id = Number(data.id) -- cgit v1.2.3-70-g09d2 From d7bd294666cba08b6f6a8d447fbdf4cd59e66b2b Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Tue, 15 Jan 2019 18:39:24 +0300 Subject: migrated some tests to normalizer, fixed some potential bug, fixed tests to use normalized naming instead of raw qvitter api objects. needs more tests tho. --- src/modules/statuses.js | 3 + src/services/api/api.service.js | 2 +- .../entity_normalizer/entity_normalizer.service.js | 212 +++++++++++++++++++++ .../status_normalizer/status_normalizer.service.js | 209 -------------------- test/unit/specs/modules/statuses.spec.js | 64 ++----- .../entity_normalizer/entity_normalizer.spec.js | 102 ++++++++++ 6 files changed, 337 insertions(+), 255 deletions(-) create mode 100644 src/services/entity_normalizer/entity_normalizer.service.js delete mode 100644 src/services/status_normalizer/status_normalizer.service.js create mode 100644 test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js (limited to 'src/services/api/api.service.js') diff --git a/src/modules/statuses.js b/src/modules/statuses.js index fde019b7..2ece12ba 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -225,6 +225,9 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us remove(timelineObject.visibleStatuses, { uri }) } }, + 'follow': (follow) => { + // NOOP, it is known status but we don't do anything about it for now + }, 'default': (unknown) => { console.log('unknown status type') console.log(unknown) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index e82f4f81..14a526ef 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -44,7 +44,7 @@ const SUGGESTIONS_URL = '/api/v1/suggestions' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' import { each, map } from 'lodash' -import { parseStatus, parseUser, parseNotification } from '../status_normalizer/status_normalizer.service.js' +import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js' import 'whatwg-fetch' const oldfetch = window.fetch diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js new file mode 100644 index 00000000..adc7f047 --- /dev/null +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -0,0 +1,212 @@ +const qvitterStatusType = (status) => { + if (status.is_post_verb) { + return 'status' + } + + if (status.retweeted_status) { + return 'retweet' + } + + if ((typeof status.uri === 'string' && status.uri.match(/(fave|objectType=Favourite)/)) || + (typeof status.text === 'string' && status.text.match(/favorited/))) { + return 'favorite' + } + + if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) { + return 'deletion' + } + + if (status.text.match(/started following/) || status.activity_type === 'follow') { + return 'follow' + } + + return 'unknown' +} + +export const parseUser = (data) => { + const output = {} + const masto = data.hasOwnProperty('acct') + // case for users in "mentions" property for statuses in MastoAPI + const mastoShort = masto && !data.hasOwnProperty('avatar') + + output.id = data.id + + if (masto) { + output.screen_name = data.acct + + // There's nothing else to get + if (mastoShort) { + return output + } + + output.name = null // missing + output.name_html = data.display_name + + output.description = null // missing + output.description_html = data.note + + // Utilize avatar_static for gif avatars? + output.profile_image_url = data.avatar + output.profile_image_url_original = data.avatar + + // Same, utilize header_static? + output.cover_photo = data.header + + output.friends_count = data.following_count + + output.bot = data.bot + + output.statusnet_profile_url = data.url + + // Missing, trying to recover + output.is_local = !output.screen_name.includes('@') + } else { + output.screen_name = data.screen_name + + output.name = data.name + output.name_html = data.name_html + + output.description = data.description + output.description_html = data.description_html + + output.profile_image_url = data.profile_image_url + output.profile_image_url_original = data.profile_image_url_original + + output.cover_photo = data.cover_photo + + output.friends_count = data.friends_count + + output.bot = null // missing + + output.statusnet_profile_url = data.statusnet_profile_url + output.is_local = data.is_local + } + + output.created_at = new Date(data.created_at) + output.locked = data.locked + output.followers_count = data.followers_count + output.statuses_count = data.statuses_count + + return output +} + +const parseAttachment = (data) => { + // TODO A little bit messy ATM but works with both APIs + return { + ...data, + mimetype: data.mimetype || data.type + } +} + +export const parseStatus = (data) => { + const output = {} + const masto = data.hasOwnProperty('account') + + if (masto) { + output.favorited = data.favourited + output.fave_num = data.favourites_count + + output.repeated = data.reblogged + output.repeat_num = data.reblogs_count + + output.type = data.reblog ? 'retweet' : 'status' + output.nsfw = data.sensitive + + output.statusnet_html = data.content + + // Not exactly the same but works? + output.text = data.content + + output.in_reply_to_status_id = data.in_reply_to_id + output.in_reply_to_user_id = data.in_reply_to_user_id + + // Not exactly the same but works + output.statusnet_conversation_id = data.id + } else { + output.favorited = data.favorited + output.fave_num = data.fave_num + + output.repeated = data.repeated + output.repeat_num = data.repeat_num + + // catchall, temporary + // Object.assign(output, data) + + output.type = qvitterStatusType(data) + + if (data.nsfw === undefined) { + output.nsfw = isNsfw(data) + if (data.retweeted_status) { + output.nsfw = data.retweeted_status.nsfw + } + } else { + output.nsfw = data.nsfw + } + + output.statusnet_html = data.statusnet_html + output.text = data.text + + output.in_reply_to_status_id = data.in_reply_to_id + output.in_reply_to_user_id = data.in_reply_to_account_id + + output.statusnet_conversation_id = data.statusnet_conversation_id + } + + output.id = Number(data.id) + output.visibility = data.visibility + output.created_at = new Date(data.created_at) + + output.user = parseUser(masto ? data.account : data.user) + + output.attentions = ((masto ? data.mentions : data.attentions) || []) + .map(_ => ({ + id: _.id, + following: _.following // FIXME: MastoAPI doesn't have this + })) + + output.attachments = ((masto ? data.media_attachments : data.attachments) || []) + .map(parseAttachment) + + const retweetedStatus = masto ? data.reblog : data.retweeted_status + if (retweetedStatus) { + output.retweeted_status = parseStatus(retweetedStatus) + } + + return output +} + +export const parseNotification = (data) => { + const mastoDict = { + 'favourite': 'like', + 'reblog': 'repeat' + } + const masto = !data.hasOwnProperty('ntype') + const output = {} + + if (masto) { + output.type = mastoDict[data.type] || data.type + output.seen = null // missing + output.status = parseStatus(data.status) + output.action = null // missing + output.from_profile = parseUser(data.account) + } else { + const parsedNotice = parseStatus(data.notice) + output.type = data.ntype + output.seen = data.is_seen + output.status = output.type === 'like' + ? parseStatus(data.notice.favorited_status) + : parsedNotice + output.action = parsedNotice + output.from_profile = parseUser(data.from_profile) + } + + output.created_at = new Date(data.created_at) + output.id = data.id + + return output +} + +const isNsfw = (status) => { + const nsfwRegex = /#nsfw/i + return (status.tags || []).includes('nsfw') || !!status.text.match(nsfwRegex) +} diff --git a/src/services/status_normalizer/status_normalizer.service.js b/src/services/status_normalizer/status_normalizer.service.js deleted file mode 100644 index 784c7f20..00000000 --- a/src/services/status_normalizer/status_normalizer.service.js +++ /dev/null @@ -1,209 +0,0 @@ -const qvitterStatusType = (status) => { - if (status.is_post_verb) { - return 'status' - } - - if (status.retweeted_status) { - return 'retweet' - } - - if ((typeof status.uri === 'string' && status.uri.match(/(fave|objectType=Favourite)/)) || - (typeof status.text === 'string' && status.text.match(/favorited/))) { - return 'favorite' - } - - if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) { - return 'deletion' - } - - if (status.text.match(/started following/) || status.activity_type === 'follow') { - return 'follow' - } - - return 'unknown' -} - -export const parseUser = (data) => { - const output = {} - const masto = data.hasOwnProperty('acct') - // case for users in "mentions" property for statuses in MastoAPI - const mastoShort = masto && !data.hasOwnProperty('avatar') - - output.id = data.id - - if (masto) { - output.screen_name = data.acct - - // There's nothing else to get - if (mastoShort) { - return output - } - - output.name = null // missing - output.name_html = data.display_name - - output.description = null // missing - output.description_html = data.note - - // Utilize avatar_static for gif avatars? - output.profile_image_url = data.avatar - output.profile_image_url_original = data.avatar - - // Same, utilize header_static? - output.cover_photo = data.header - - output.friends_count = data.following_count - - output.bot = data.bot - - output.statusnet_profile_url = data.url - - // Missing, trying to recover - output.is_local = !output.screen_name.includes('@') - } else { - output.screen_name = data.screen_name - - output.name = data.name - output.name_html = data.name_html - - output.description = data.description - output.description_html = data.description_html - - output.profile_image_url = data.profile_image_url - output.profile_image_url_original = data.profile_image_url_original - - output.cover_photo = data.cover_photo - - output.friends_count = data.friends_count - - output.bot = null // missing - - output.statusnet_profile_url = data.statusnet_profile_url - output.is_local = data.is_local - } - - output.created_at = new Date(data.created_at) - output.locked = data.locked - output.followers_count = data.followers_count - output.statuses_count = data.statuses_count - - return output -} - -const parseAttachment = (data) => { - // TODO A little bit messy ATM but works with both APIs - return { - ...data, - mimetype: data.mimetype || data.type - } -} - -export const parseStatus = (data) => { - const output = {} - const masto = data.hasOwnProperty('account') - - if (masto) { - output.favorited = data.favourited - output.fave_num = data.favourites_count - - output.repeated = data.reblogged - output.repeat_num = data.reblogs_count - - output.type = data.reblog ? 'retweet' : 'status' - output.nsfw = data.sensitive - - output.statusnet_html = data.content - - // Not exactly the same but works? - output.text = data.content - - output.in_reply_to_status_id = data.in_reply_to_id - output.in_reply_to_user_id = data.in_reply_to_user_id - - // Not exactly the same but works - output.statusnet_conversation_id = data.id - } else { - output.favorited = data.favorited - output.fave_num = data.fave_num - - output.repeated = data.repeated - output.repeat_num = data.repeat_num - - // catchall, temporary - // Object.assign(output, data) - - output.type = qvitterStatusType(data) - - if (data.nsfw === undefined) { - output.nsfw = isNsfw(data) - if (data.retweeted_status) { - output.nsfw = data.retweeted_status.nsfw - } - } - output.statusnet_html = data.statusnet_html - output.text = data.text - - output.in_reply_to_status_id = data.in_reply_to_id - output.in_reply_to_user_id = data.in_reply_to_account_id - - output.statusnet_conversation_id = data.statusnet_conversation_id - } - - output.id = Number(data.id) - output.visibility = data.visibility - output.created_at = new Date(data.created_at) - - output.user = parseUser(masto ? data.account : data.user) - - output.attentions = ((masto ? data.mentions : data.attentions) || []) - .map(_ => ({ - id: _.id, - following: _.following // FIXME: MastoAPI doesn't have this - })) - - output.attachments = ((masto ? data.media_attachments : data.attachments) || []) - .map(parseAttachment) - - const retweetedStatus = masto ? data.reblog : data.retweeted_status - if (retweetedStatus) { - output.retweeted_status = parseStatus(retweetedStatus) - } - - return output -} - -export const parseNotification = (data) => { - const mastoDict = { - 'favourite': 'like', - 'reblog': 'repeat' - } - const masto = !data.hasOwnProperty('ntype') - const output = {} - - if (masto) { - output.type = mastoDict[data.type] || data.type - output.seen = null // missing - output.status = parseStatus(data.status) - output.action = null // missing - output.from_profile = parseUser(data.account) - } else { - const parsedNotice = parseStatus(data.notice) - output.type = data.ntype - output.seen = data.is_seen - output.status = output.type === 'like' - ? parseStatus(data.notice.favorited_status) - : parsedNotice - output.action = parsedNotice - output.from_profile = parseUser(data.from_profile) - } - - output.created_at = new Date(data.created_at) - output.id = data.id - - return output -} - -const isNsfw = (status) => { - const nsfwRegex = /#nsfw/i - return (status.tags || []).includes('nsfw') || !!status.text.match(nsfwRegex) -} diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js index 7d403312..0b511b80 100644 --- a/test/unit/specs/modules/statuses.spec.js +++ b/test/unit/specs/modules/statuses.spec.js @@ -1,8 +1,8 @@ import { cloneDeep } from 'lodash' -import { defaultState, mutations, findMaxId, prepareStatus, statusType } from '../../../../src/modules/statuses.js' +import { defaultState, mutations, findMaxId, prepareStatus } from '../../../../src/modules/statuses.js' // eslint-disable-next-line camelcase -const makeMockStatus = ({id, text, is_post_verb = true}) => { +const makeMockStatus = ({id, text, type = 'status'}) => { return { id, user: {id: 0}, @@ -10,42 +10,12 @@ const makeMockStatus = ({id, text, is_post_verb = true}) => { text: text || `Text number ${id}`, fave_num: 0, uri: '', - is_post_verb, + type, attentions: [] } } -describe('Statuses.statusType', () => { - it('identifies favorites', () => { - const fav = { - uri: 'tag:soykaf.com,2016-08-21:fave:2558:note:339495:2016-08-21T16:54:04+00:00' - } - - const mastoFav = { - uri: 'tag:mastodon.social,2016-11-27:objectId=73903:objectType=Favourite' - } - - expect(statusType(fav)).to.eql('favorite') - expect(statusType(mastoFav)).to.eql('favorite') - }) -}) - describe('Statuses.prepareStatus', () => { - it('sets nsfw for statuses with the #nsfw tag', () => { - const safe = makeMockStatus({id: 1, text: 'Hello oniichan'}) - const nsfw = makeMockStatus({id: 1, text: 'Hello oniichan #nsfw'}) - - expect(prepareStatus(safe).nsfw).to.eq(false) - expect(prepareStatus(nsfw).nsfw).to.eq(true) - }) - - it('leaves existing nsfw settings alone', () => { - const nsfw = makeMockStatus({id: 1, text: 'Hello oniichan #nsfw'}) - nsfw.nsfw = false - - expect(prepareStatus(nsfw).nsfw).to.eq(false) - }) - it('sets deleted flag to false', () => { const aStatus = makeMockStatus({id: 1, text: 'Hello oniichan'}) expect(prepareStatus(aStatus).deleted).to.eq(false) @@ -127,7 +97,7 @@ describe('The Statuses module', () => { const status = makeMockStatus({id: 1}) const otherStatus = makeMockStatus({id: 3}) status.uri = 'xxx' - const deletion = makeMockStatus({id: 2, is_post_verb: false}) + const deletion = makeMockStatus({id: 2, type: 'deletion'}) deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.' deletion.uri = 'xxx' @@ -177,7 +147,7 @@ describe('The Statuses module', () => { it('splits retweets from their status and links them', () => { const state = cloneDeep(defaultState) const status = makeMockStatus({id: 1}) - const retweet = makeMockStatus({id: 2, is_post_verb: false}) + const retweet = makeMockStatus({id: 2, type: 'retweet'}) const modStatus = makeMockStatus({id: 1, text: 'something else'}) retweet.retweeted_status = status @@ -220,7 +190,7 @@ describe('The Statuses module', () => { const state = cloneDeep(defaultState) const status = makeMockStatus({id: 1}) const modStatus = makeMockStatus({id: 1, text: 'something else'}) - const retweet = makeMockStatus({id: 2, is_post_verb: false}) + const retweet = makeMockStatus({id: 2, type: 'retweet'}) retweet.retweeted_status = modStatus // Add original status @@ -243,7 +213,7 @@ describe('The Statuses module', () => { const favorite = { id: 2, - is_post_verb: false, + type: 'favorite', in_reply_to_status_id: '1', // The API uses strings here... uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00', text: 'a favorited something by b', @@ -271,7 +241,7 @@ describe('The Statuses module', () => { const ownFavorite = { id: 3, - is_post_verb: false, + type: 'favorite', in_reply_to_status_id: '1', // The API uses strings here... uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00', text: 'a favorited something by b', @@ -296,7 +266,7 @@ describe('The Statuses module', () => { mentionedStatus.uri = 'xxx' otherStatus.attentions = [user] - const deletion = makeMockStatus({id: 4, is_post_verb: false}) + const deletion = makeMockStatus({id: 4, type: 'deletion'}) deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.' deletion.uri = 'xxx' @@ -305,10 +275,12 @@ describe('The Statuses module', () => { state, { notifications: [{ - ntype: 'mention', + from_profile: { id: 2 }, + id: 998, + type: 'mention', status: otherStatus, - notice: otherStatus, - is_seen: false + action: otherStatus, + seen: false }] }) @@ -317,10 +289,12 @@ describe('The Statuses module', () => { state, { notifications: [{ - ntype: 'mention', + from_profile: { id: 2 }, + id: 999, + type: 'mention', status: mentionedStatus, - notice: mentionedStatus, - is_seen: false + action: mentionedStatus, + seen: false }] }) diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js new file mode 100644 index 00000000..e5882725 --- /dev/null +++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js @@ -0,0 +1,102 @@ +import { parseStatus, parseUser, parseNotification } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' + +const makeMockStatusQvitter = (overrides = {}) => { + return Object.assign({ + activity_type: 'post', + attachments: [], + attentions: [], + created_at: 'Tue Jan 15 13:57:56 +0000 2019', + external_url: 'https://ap.example/whatever', + fave_num: 1, + favorited: false, + id: 10335970, + in_reply_to_ostatus_uri: null, + in_reply_to_profileurl: null, + in_reply_to_screen_name: null, + in_reply_to_status_id: null, + in_reply_to_user_id: null, + is_local: false, + is_post_verb: true, + possibly_sensitive: false, + repeat_num: 0, + repeated: false, + statusnet_conversation_id: 16300488, + statusnet_html: '

haha benis

', + summary: null, + tags: [], + text: 'haha benis', + uri: 'https://ap.example/whatever', + user: makeMockUserQvitter(), + visibility: 'public' + }, overrides) +} + +const makeMockUserQvitter = (overrides = {}) => { + return Object.assign({ + background_image: null, + cover_photo: '', + created_at: 'Mon Jan 14 13:56:51 +0000 2019', + default_scope: 'public', + description: 'ebin', + description_html: '

ebin

', + favourites_count: 0, + fields: [], + followers_count: 1, + following: true, + follows_you: true, + friends_count: 1, + id: 60717, + is_local: false, + locked: false, + name: 'Spurdo :ebin:', + name_html: 'Spurdo ', + no_rich_text: false, + pleroma: { confirmation_pending: false, tags: [] }, + profile_image_url: 'https://ap.example/whatever', + profile_image_url_https: 'https://ap.example/whatever', + profile_image_url_original: 'https://ap.example/whatever', + profile_image_url_profile_size: 'https://ap.example/whatever', + rights: { delete_others_notice: false }, + screen_name: 'spurdo@ap.example', + statuses_count: 46, + statusnet_blocking: false, + statusnet_profile_url: '' + }, overrides) +} + +parseNotification +parseUser +parseStatus +makeMockStatusQvitter +makeMockUserQvitter + +describe('QVitter preprocessing', () => { + it('identifies favorites', () => { + const fav = { + uri: 'tag:soykaf.com,2016-08-21:fave:2558:note:339495:2016-08-21T16:54:04+00:00', + is_post_verb: false + } + + const mastoFav = { + uri: 'tag:mastodon.social,2016-11-27:objectId=73903:objectType=Favourite', + is_post_verb: false + } + + expect(parseStatus(makeMockStatusQvitter(fav))).to.have.property('type', 'favorite') + expect(parseStatus(makeMockStatusQvitter(mastoFav))).to.have.property('type', 'favorite') + }) + + it('sets nsfw for statuses with the #nsfw tag', () => { + const safe = makeMockStatusQvitter({id: 1, text: 'Hello oniichan'}) + const nsfw = makeMockStatusQvitter({id: 1, text: 'Hello oniichan #nsfw'}) + + expect(parseStatus(safe).nsfw).to.eq(false) + expect(parseStatus(nsfw).nsfw).to.eq(true) + }) + + it('leaves existing nsfw settings alone', () => { + const nsfw = makeMockStatusQvitter({id: 1, text: 'Hello oniichan #nsfw', nsfw: false}) + + expect(parseStatus(nsfw).nsfw).to.eq(false) + }) +}) -- cgit v1.2.3-70-g09d2 From cab87744c830e4411ec20c1dcb2d454d657219bc Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 17 Jan 2019 21:46:03 +0300 Subject: Revert "some initial work to make it possible to use "unregistered" timelines, i.e. not" and some stuff to make favorites still work This reverts commit 039a4074006fb91ac9031b41b4e9af4a15766dfa. --- src/components/timeline/timeline.js | 17 +++++----- src/components/user_profile/user_profile.js | 9 ++---- src/modules/api.js | 10 +++--- src/modules/statuses.js | 36 ++++++++++------------ src/services/api/api.service.js | 5 ++- .../timeline_fetcher/timeline_fetcher.service.js | 4 +-- 6 files changed, 38 insertions(+), 43 deletions(-) (limited to 'src/services/api/api.service.js') diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 23d2c1e8..98da8660 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -7,6 +7,7 @@ import { throttle } from 'lodash' const Timeline = { props: [ 'timeline', + 'timelineName', 'title', 'userId', 'tag', @@ -54,7 +55,7 @@ const Timeline = { timelineFetcher.fetchAndUpdate({ store, credentials, - timeline: this.timeline, + timeline: this.timelineName, showImmediately, userId: this.userId, tag: this.tag @@ -69,32 +70,32 @@ const Timeline = { destroyed () { window.removeEventListener('scroll', this.scrollLoad) if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false) - this.$store.commit('setLoading', { timeline: this.timeline, value: false }) + this.$store.commit('setLoading', { timeline: this.timelineName, value: false }) }, methods: { showNewStatuses () { if (this.timeline.flushMarker !== 0) { - this.$store.commit('clearTimeline', { timeline: this.timeline }) - this.$store.commit('queueFlush', { timeline: this.timeline, id: 0 }) + this.$store.commit('clearTimeline', { timeline: this.timelineName }) + this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 }) this.fetchOlderStatuses() } else { - this.$store.commit('showNewStatuses', { timeline: this.timeline }) + this.$store.commit('showNewStatuses', { timeline: this.timelineName }) this.paused = false } }, fetchOlderStatuses: throttle(function () { const store = this.$store const credentials = store.state.users.currentUser.credentials - store.commit('setLoading', { timeline: this.timeline, value: true }) + store.commit('setLoading', { timeline: this.timelineName, value: true }) timelineFetcher.fetchAndUpdate({ store, credentials, - timeline: this.timeline, + timeline: this.timelineName, older: true, showImmediately: true, userId: this.userId, tag: this.tag - }).then(() => store.commit('setLoading', { timeline: this.timeline, value: false })) + }).then(() => store.commit('setLoading', { timeline: this.timelineName, value: false })) }, 1000, this), scrollLoad (e) { const bodyBRect = document.body.getBoundingClientRect() diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 245d55ca..7f17ef69 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -1,7 +1,6 @@ import UserCardContent from '../user_card_content/user_card_content.vue' import UserCard from '../user_card/user_card.vue' import Timeline from '../timeline/timeline.vue' -import { emptyTl } from '../../modules/statuses.js' const UserProfile = { created () { @@ -14,15 +13,13 @@ const UserProfile = { destroyed () { this.$store.dispatch('stopFetching', 'user') }, - data () { - return { - favorites: emptyTl({ type: 'favorites', userId: this.userId }) - } - }, computed: { timeline () { return this.$store.state.statuses.timelines.user }, + favorites () { + return this.$store.state.statuses.timelines.favorites + }, userId () { return this.$route.params.id || this.user.id }, diff --git a/src/modules/api.js b/src/modules/api.js index b85b24be..a61340c2 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -5,7 +5,7 @@ import { Socket } from 'phoenix' const api = { state: { backendInteractor: backendInteractorService(), - fetchers: new Map(), + fetchers: {}, socket: null, chatDisabled: false, followRequests: [] @@ -15,10 +15,10 @@ const api = { state.backendInteractor = backendInteractor }, addFetcher (state, {timeline, fetcher}) { - state.fetchers.set(timeline, fetcher) + state.fetchers[timeline] = fetcher }, removeFetcher (state, {timeline}) { - delete state.fetchers.delete(timeline) + delete state.fetchers[timeline] }, setSocket (state, socket) { state.socket = socket @@ -41,13 +41,13 @@ const api = { } // Don't start fetching if we already are. - if (!store.state.fetchers.has(timeline)) { + if (!store.state.fetchers[timeline]) { const fetcher = store.state.backendInteractor.startFetching({timeline, store, userId}) store.commit('addFetcher', {timeline, fetcher}) } }, stopFetching (store, timeline) { - const fetcher = store.state.fetchers.get(timeline) + const fetcher = store.state.fetchers[timeline] window.clearInterval(fetcher) store.commit('removeFetcher', {timeline}) }, diff --git a/src/modules/statuses.js b/src/modules/statuses.js index baeef8bf..f976fa42 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -2,7 +2,7 @@ import { remove, slice, each, find, maxBy, minBy, merge, last, isArray } from 'l import apiService from '../services/api/api.service.js' // import parse from '../services/status_parser/status_parser.js' -export const emptyTl = (tl, userId = 0) => (Object.assign(tl, { +const emptyTl = () => ({ statuses: [], statusesObject: {}, faves: [], @@ -14,9 +14,9 @@ export const emptyTl = (tl, userId = 0) => (Object.assign(tl, { loading: false, followers: [], friends: [], - flushMarker: 0, - userId -})) + userId: 0, + flushMarker: 0 +}) export const defaultState = { allStatuses: [], @@ -33,13 +33,14 @@ export const defaultState = { favorites: new Set(), error: false, timelines: { - mentions: emptyTl({ type: 'mentions' }), - public: emptyTl({ type: 'public' }), - user: emptyTl({ type: 'user' }), // TODO: switch to unregistered - publicAndExternal: emptyTl({ type: 'publicAndExternal' }), - friends: emptyTl({ type: 'friends' }), - tag: emptyTl({ type: 'tag' }), - dms: emptyTl({ type: 'dms' }) + mentions: emptyTl(), + public: emptyTl(), + user: emptyTl(), + favorites: emptyTl(), + publicAndExternal: emptyTl(), + friends: emptyTl(), + tag: emptyTl(), + dms: emptyTl() } } @@ -100,7 +101,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us const allStatuses = state.allStatuses const allStatusesObject = state.allStatusesObject - const timelineObject = typeof timeline === 'object' ? timeline : state.timelines[timeline] + const timelineObject = state.timelines[timeline] const maxNew = statuses.length > 0 ? maxBy(statuses, 'id').id : 0 const older = timeline && maxNew < timelineObject.maxId @@ -297,7 +298,7 @@ export const mutations = { addNewStatuses, addNewNotifications, showNewStatuses (state, { timeline }) { - const oldTimeline = (typeof timeline === 'object' ? timeline : state.timelines[timeline]) + const oldTimeline = (state.timelines[timeline]) oldTimeline.newStatusCount = 0 oldTimeline.visibleStatuses = slice(oldTimeline.statuses, 0, 50) @@ -306,8 +307,7 @@ export const mutations = { each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status }) }, clearTimeline (state, { timeline }) { - const timelineObject = typeof timeline === 'object' ? timeline : state.timelines[timeline] - emptyTl(timelineObject, timeline.userId) + state.timelines[timeline] = emptyTl() }, setFavorited (state, { status, value }) { const newStatus = state.allStatusesObject[status.id] @@ -327,8 +327,7 @@ export const mutations = { newStatus.deleted = true }, setLoading (state, { timeline, value }) { - const timelineObject = typeof timeline === 'object' ? timeline : state.timelines[timeline] - timelineObject.loading = value + state.timelines[timeline].loading = value }, setNsfw (state, { id, nsfw }) { const newStatus = state.allStatusesObject[id] @@ -349,8 +348,7 @@ export const mutations = { }) }, queueFlush (state, { timeline, id }) { - const timelineObject = typeof timeline === 'object' ? timeline : state.timelines[timeline] - timelineObject.flushMarker = id + state.timelines[timeline].flushMarker = id } } diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 14a526ef..0e267276 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -327,11 +327,10 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, tag: TAG_TIMELINE_URL } - const type = timeline.type || timeline - const isNotifications = type === 'notifications' + const isNotifications = timeline === 'notifications' const params = [] - let url = timelineUrls[type] + let url = timelineUrls[timeline] if (since) { params.push(['since_id', since]) diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 126e07cf..727f6c60 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -3,7 +3,7 @@ import { camelCase } from 'lodash' import apiService from '../api/api.service.js' const update = ({store, statuses, timeline, showImmediately, userId}) => { - const ccTimeline = typeof timeline === 'object' ? timeline : camelCase(timeline) + const ccTimeline = camelCase(timeline) store.dispatch('setError', { value: false }) @@ -18,7 +18,7 @@ const update = ({store, statuses, timeline, showImmediately, userId}) => { const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false, showImmediately = false, userId = false, tag = false, until}) => { const args = { timeline, credentials } const rootState = store.rootState || store.state - const timelineData = typeof timeline === 'object' ? timeline : rootState.statuses.timelines[camelCase(timeline)] + const timelineData = rootState.statuses.timelines[camelCase(timeline)] if (older) { args['until'] = until || timelineData.minVisibleId -- cgit v1.2.3-70-g09d2 From 93cbb58212ebb83cee5bc89f8cef1ebb58969f5c Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 17 Jan 2019 22:11:51 +0300 Subject: fix login and favorites tab... --- src/components/user_profile/user_profile.js | 13 +++++ src/components/user_profile/user_profile.vue | 2 +- src/modules/users.js | 61 +++++++++++----------- src/services/api/api.service.js | 12 +++++ .../entity_normalizer/entity_normalizer.service.js | 8 +++ 5 files changed, 64 insertions(+), 32 deletions(-) (limited to 'src/services/api/api.service.js') diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 7f17ef69..c9197a1c 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -5,13 +5,16 @@ import Timeline from '../timeline/timeline.vue' const UserProfile = { created () { this.$store.commit('clearTimeline', { timeline: 'user' }) + this.$store.commit('clearTimeline', { timeline: 'favorites' }) this.$store.dispatch('startFetching', ['user', this.fetchBy]) + this.$store.dispatch('startFetching', ['favorites', this.fetchBy]) if (!this.user.id) { this.$store.dispatch('fetchUser', this.fetchBy) } }, destroyed () { this.$store.dispatch('stopFetching', 'user') + this.$store.dispatch('stopFetching', 'favorites') }, computed: { timeline () { @@ -26,6 +29,9 @@ const UserProfile = { userName () { return this.$route.params.name || this.user.screen_name }, + isUs () { + return this.userId === this.$store.state.users.currentUser.id + }, friends () { return this.user.friends }, @@ -65,21 +71,28 @@ const UserProfile = { } }, watch: { + // TODO get rid of this copypasta userName () { if (this.isExternal) { return } this.$store.dispatch('stopFetching', 'user') + this.$store.dispatch('stopFetching', 'favorites') this.$store.commit('clearTimeline', { timeline: 'user' }) + this.$store.commit('clearTimeline', { timeline: 'favorites' }) this.$store.dispatch('startFetching', ['user', this.fetchBy]) + this.$store.dispatch('startFetching', ['favorites', this.fetchBy]) }, userId () { if (!this.isExternal) { return } this.$store.dispatch('stopFetching', 'user') + this.$store.dispatch('stopFetching', 'favorites') this.$store.commit('clearTimeline', { timeline: 'user' }) + this.$store.commit('clearTimeline', { timeline: 'favorites' }) this.$store.dispatch('startFetching', ['user', this.fetchBy]) + this.$store.dispatch('startFetching', ['favorites', this.fetchBy]) }, user () { if (this.user.id && !this.user.followers) { diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index 265fc65b..e53727ff 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -20,7 +20,7 @@ - +