From b27ec058cae582e8b9ad9e56be0e9be57f865886 Mon Sep 17 00:00:00 2001 From: William Pitcock Date: Sun, 27 Jan 2019 12:56:07 +0000 Subject: [PATCH 01/20] entity normalizer: add support for opengraph cards --- src/services/entity_normalizer/entity_normalizer.service.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index e69547b6f8..deffa53768 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -215,6 +215,7 @@ export const parseStatus = (data) => { output.id = String(data.id) output.visibility = data.visibility + output.card = data.card output.created_at = new Date(data.created_at) // Converting to string, the right way. From 0924907c64376249433e2fd16278662181c9e856 Mon Sep 17 00:00:00 2001 From: William Pitcock Date: Sun, 27 Jan 2019 13:47:30 +0000 Subject: [PATCH 02/20] add link-preview component --- src/components/link-preview/link-preview.js | 8 +++ src/components/link-preview/link-preview.vue | 59 ++++++++++++++++++++ src/components/status/status.js | 4 +- src/components/status/status.vue | 4 ++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/components/link-preview/link-preview.js create mode 100644 src/components/link-preview/link-preview.vue diff --git a/src/components/link-preview/link-preview.js b/src/components/link-preview/link-preview.js new file mode 100644 index 0000000000..13264afb7e --- /dev/null +++ b/src/components/link-preview/link-preview.js @@ -0,0 +1,8 @@ +const LinkPreview = { + name: 'LinkPreview', + props: [ + 'card' + ] +} + +export default LinkPreview diff --git a/src/components/link-preview/link-preview.vue b/src/components/link-preview/link-preview.vue new file mode 100644 index 0000000000..49de654f4a --- /dev/null +++ b/src/components/link-preview/link-preview.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/src/components/status/status.js b/src/components/status/status.js index b14a74ec40..681d0373f8 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -5,6 +5,7 @@ import DeleteButton from '../delete_button/delete_button.vue' import PostStatusForm from '../post_status_form/post_status_form.vue' import UserCardContent from '../user_card_content/user_card_content.vue' import StillImage from '../still-image/still-image.vue' +import LinkPreview from '../link-preview/link-preview.vue' import { filter, find } from 'lodash' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -220,7 +221,8 @@ const Status = { DeleteButton, PostStatusForm, UserCardContent, - StillImage + StillImage, + LinkPreview }, methods: { visibilityIcon (visibility) { diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 5c956467e2..e959544b2f 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -98,6 +98,10 @@ + +
From 2b86f6e883878bcec624d4a99aadac81d87f2826 Mon Sep 17 00:00:00 2001 From: William Pitcock Date: Sun, 27 Jan 2019 13:57:37 +0000 Subject: [PATCH 03/20] status: only show link preview in main post view --- src/components/status/status.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/status/status.vue b/src/components/status/status.vue index e959544b2f..aa37ad5161 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -98,7 +98,7 @@
- diff --git a/src/i18n/de.json b/src/i18n/de.json index c87371e6e9..82860e9ebc 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -155,7 +155,8 @@ "notification_visibility_mentions": "Erwähnungen", "notification_visibility_repeats": "Wiederholungen", "no_rich_text_description": "Rich-Text Formatierungen von allen Beiträgen entfernen", - "hide_network_description": "Zeige nicht, wem ich folge und wer mir folgt", + "hide_followings_description": "Zeige nicht, wem ich folge", + "hide_followers_description": "Zeige nicht, wer mir folgt", "nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind", "panelRadius": "Panel", "pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist", diff --git a/src/i18n/en.json b/src/i18n/en.json index 3ff98ab0c3..a2106f1a38 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -157,7 +157,8 @@ "notification_visibility_mentions": "Mentions", "notification_visibility_repeats": "Repeats", "no_rich_text_description": "Strip rich text formatting from all posts", - "hide_network_description": "Don't show who I'm following and who's following me", + "hide_followings_description": "Don't show who I'm following", + "hide_followers_description": "Don't show who's following me", "nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding", "panelRadius": "Panels", "pause_on_unfocused": "Pause streaming when tab is not focused", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index 161856f036..37e96f6cae 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -157,7 +157,8 @@ "notification_visibility_mentions": "メンション", "notification_visibility_repeats": "リピート", "no_rich_text_description": "リッチテキストをつかわない", - "hide_network_description": "わたしがフォローしているひとと、わたしをフォローしているひとを、みせない", + "hide_followings_description": "フォローしている人を表示しない", + "hide_followers_description": "フォローしている人を表示しない", "nsfw_clickthrough": "NSFWなファイルをかくす", "panelRadius": "パネル", "pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index 4b69df0734..9f40be51a2 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -156,7 +156,8 @@ "notification_visibility_mentions": "멘션", "notification_visibility_repeats": "반복", "no_rich_text_description": "모든 게시물의 서식을 지우기", - "hide_network_description": "내 팔로우와 팔로워를 숨기기", + "hide_followings_description": "내가 팔로우하는 사람을 표시하지 않음", + "hide_followers_description": "나를 따르는 사람을 보여주지 마라.", "nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화", "panelRadius": "패널", "pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 0887bb5908..bf1e319fa4 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -127,7 +127,8 @@ "notification_visibility_mentions": "Упоминания", "notification_visibility_repeats": "Повторы", "no_rich_text_description": "Убрать форматирование из всех постов", - "hide_network_description": "Не показывать кого я читаю и кто меня читает", + "hide_followings_description": "Не показывать кого я читаю", + "hide_followers_description": "Не показывать кто читает меня", "nsfw_clickthrough": "Включить скрытие NSFW вложений", "panelRadius": "Панели", "pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе", diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 5b0d865025..31b48cb6fe 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -130,7 +130,7 @@ const updateBanner = ({credentials, params}) => { // description const updateProfile = ({credentials, params}) => { // Always include these fields, because they might be empty or false - const fields = ['description', 'locked', 'no_rich_text', 'hide_network'] + const fields = ['description', 'locked', 'no_rich_text', 'hide_followings', 'hide_followers'] let url = PROFILE_UPDATE_URL const form = new FormData() diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index deffa53768..fa955ad6cd 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -100,7 +100,8 @@ export const parseUser = (data) => { output.rights = data.rights output.no_rich_text = data.no_rich_text output.default_scope = data.default_scope - output.hide_network = data.hide_network + output.hide_followings = data.hide_followings + output.hide_followers = data.hide_followers output.background_image = data.background_image // on mastoapi this info is contained in a "relationship" output.following = data.following From 0a39159fdf99143ddf05580f066508e6d5a90773 Mon Sep 17 00:00:00 2001 From: shpuld Date: Wed, 30 Jan 2019 16:38:28 +0200 Subject: [PATCH 18/20] Adjust scrolling logic and document it, make sure to never show 'show less' if it's not a tall status --- src/components/status/status.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/status/status.js b/src/components/status/status.js index 558125dff1..6ef13010da 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -179,7 +179,7 @@ const Status = { return this.tallStatus }, showingMore () { - return this.showingTall || (this.status.summary && this.expandingSubject) + return (this.tallStatus && this.showingTall) || (this.status.summary && this.expandingSubject) }, nsfwClickthrough () { if (!this.status.nsfw) { @@ -303,11 +303,14 @@ const Status = { 'highlight': function (id) { if (this.status.id === id) { let rect = this.$el.getBoundingClientRect() - if (rect.top < 140) { - window.scrollBy(0, rect.top - 200) - } else if (rect.top < window.innerHeight && rect.height >= (window.innerHeight - 50)) { - window.scrollBy(0, rect.top - 50) + if (rect.top < 100) { + // Post is above screen, match its top to screen top + window.scrollBy(0, rect.top - 100) + } else if (rect.height >= (window.innerHeight - 50)) { + // Post we want to see is taller than screen so match its top to screen top + window.scrollBy(0, rect.top - 100) } else if (rect.bottom > window.innerHeight - 50) { + // Post is below screen, match its bottom to screen bottom window.scrollBy(0, rect.bottom - window.innerHeight + 50) } } From 16a5272726ec555d49276e291caa1d0ba71200e4 Mon Sep 17 00:00:00 2001 From: shpuld Date: Wed, 30 Jan 2019 16:57:19 +0200 Subject: [PATCH 19/20] Fix a simple typo --- src/components/status/status.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/status/status.js b/src/components/status/status.js index 558125dff1..f85b784747 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -79,7 +79,7 @@ const Status = { }, replyProfileLink () { if (this.isReply) { - return this.generateUserProfileLink(this.status.in_reply_to_status_id, this.replyToName) + return this.generateUserProfileLink(this.status.in_reply_to_user_id, this.replyToName) } }, retweet () { return !!this.statusoid.retweeted_status }, From 15603981f8309d979465c40175f9b3cd4f6617b4 Mon Sep 17 00:00:00 2001 From: shpuld Date: Wed, 30 Jan 2019 19:15:35 +0200 Subject: [PATCH 20/20] Capture clicks on statuses to hijack mention clicks, match mention href to user somehow --- src/components/status/status.js | 15 ++++- src/components/status/status.vue | 4 +- .../mention_matcher/mention_matcher.js | 9 +++ .../mention_matcher/mention_matcher.spec.js | 63 +++++++++++++++++++ 4 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 src/services/mention_matcher/mention_matcher.js create mode 100644 test/unit/specs/services/mention_matcher/mention_matcher.spec.js diff --git a/src/components/status/status.js b/src/components/status/status.js index 558125dff1..e268ddaa08 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -9,6 +9,7 @@ import LinkPreview from '../link-preview/link-preview.vue' import { filter, find } from 'lodash' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' +import { mentionMatchesUrl } from 'src/services/mention_matcher/mention_matcher.js' const Status = { name: 'Status', @@ -237,11 +238,23 @@ const Status = { return 'icon-globe' } }, - linkClicked ({target}) { + linkClicked (event) { + let { target } = event if (target.tagName === 'SPAN') { target = target.parentNode } if (target.tagName === 'A') { + if (target.className.match(/mention/)) { + const href = target.getAttribute('href') + const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href)) + if (attn) { + event.stopPropagation() + event.preventDefault() + const link = this.generateUserProfileLink(attn.id, attn.screen_name) + this.$router.push(link) + return + } + } window.open(target.href, '_blank') } }, diff --git a/src/components/status/status.vue b/src/components/status/status.vue index d88428c7aa..45100a4666 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -24,9 +24,9 @@
diff --git a/src/services/mention_matcher/mention_matcher.js b/src/services/mention_matcher/mention_matcher.js new file mode 100644 index 0000000000..2c1ed97064 --- /dev/null +++ b/src/services/mention_matcher/mention_matcher.js @@ -0,0 +1,9 @@ + +export const mentionMatchesUrl = (attention, url) => { + if (url === attention.statusnet_profile_url) { + return true + } + const [namepart, instancepart] = attention.screen_name.split('@') + const matchstring = new RegExp('://' + instancepart + '/.*' + namepart + '$', 'g') + return !!url.match(matchstring) +} diff --git a/test/unit/specs/services/mention_matcher/mention_matcher.spec.js b/test/unit/specs/services/mention_matcher/mention_matcher.spec.js new file mode 100644 index 0000000000..4f6f58fff3 --- /dev/null +++ b/test/unit/specs/services/mention_matcher/mention_matcher.spec.js @@ -0,0 +1,63 @@ +import * as MentionMatcher from 'src/services/mention_matcher/mention_matcher.js' + +const localAttn = () => ({ + id: 123, + is_local: true, + name: 'Guy', + screen_name: 'person', + statusnet_profile_url: 'https://instance.com/users/person' +}) + +const externalAttn = () => ({ + id: 123, + is_local: false, + name: 'Guy', + screen_name: 'person@instance.com', + statusnet_profile_url: 'https://instance.com/users/person' +}) + +describe('MentionMatcher', () => { + describe.only('mentionMatchesUrl', () => { + it('should match local mention', () => { + const attention = localAttn() + const url = 'https://instance.com/users/person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true) + }) + + it('should not match a local mention with same name but different instance', () => { + const attention = localAttn() + const url = 'https://website.com/users/person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false) + }) + + it('should match external pleroma mention', () => { + const attention = externalAttn() + const url = 'https://instance.com/users/person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true) + }) + + it('should not match external pleroma mention with same name but different instance', () => { + const attention = externalAttn() + const url = 'https://website.com/users/person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false) + }) + + it('should match external mastodon mention', () => { + const attention = externalAttn() + const url = 'https://instance.com/@person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true) + }) + + it('should not match external mastodon mention with same name but different instance', () => { + const attention = externalAttn() + const url = 'https://website.com/@person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false) + }) + }) +})