diff --git a/backend/public/providers.json b/backend/public/providers.json new file mode 100644 index 000000000..ef9f04bff --- /dev/null +++ b/backend/public/providers.json @@ -0,0 +1,257 @@ +[ + { + "provider_name": "Codepen", + "provider_url": "https:\/\/codepen.io", + "endpoints": [ + { + "schemes": [ + "http:\/\/codepen.io\/*", + "https:\/\/codepen.io\/*" + ], + "url": "http:\/\/codepen.io\/api\/oembed" + } + ] + }, + { + "provider_name": "DTube", + "provider_url": "https:\/\/d.tube\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/d.tube\/v\/*" + ], + "url": "https:\/\/api.d.tube\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Facebook (Post)", + "provider_url": "https:\/\/www.facebook.com\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/www.facebook.com\/*\/posts\/*", + "https:\/\/www.facebook.com\/photos\/*", + "https:\/\/www.facebook.com\/*\/photos\/*", + "https:\/\/www.facebook.com\/photo.php*", + "https:\/\/www.facebook.com\/photo.php", + "https:\/\/www.facebook.com\/*\/activity\/*", + "https:\/\/www.facebook.com\/permalink.php", + "https:\/\/www.facebook.com\/media\/set?set=*", + "https:\/\/www.facebook.com\/questions\/*", + "https:\/\/www.facebook.com\/notes\/*\/*\/*" + ], + "url": "https:\/\/www.facebook.com\/plugins\/post\/oembed.json", + "discovery": true + } + ] + }, + { + "provider_name": "Facebook (Video)", + "provider_url": "https:\/\/www.facebook.com\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/www.facebook.com\/*\/videos\/*", + "https:\/\/www.facebook.com\/video.php" + ], + "url": "https:\/\/www.facebook.com\/plugins\/video\/oembed.json", + "discovery": true + } + ] + }, + { + "provider_name": "Flickr", + "provider_url": "https:\/\/www.flickr.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/*.flickr.com\/photos\/*", + "http:\/\/flic.kr\/p\/*", + "https:\/\/*.flickr.com\/photos\/*", + "https:\/\/flic.kr\/p\/*" + ], + "url": "https:\/\/www.flickr.com\/services\/oembed\/", + "discovery": true + } + ] + }, + { + "provider_name": "GIPHY", + "provider_url": "https:\/\/giphy.com", + "endpoints": [ + { + "schemes": [ + "https:\/\/giphy.com\/gifs\/*", + "http:\/\/gph.is\/*", + "https:\/\/media.giphy.com\/media\/*\/giphy.gif" + ], + "url": "https:\/\/giphy.com\/services\/oembed", + "discovery": true + } + ] + }, + { + "provider_name": "Instagram", + "provider_url": "https:\/\/instagram.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/instagram.com\/p\/*", + "http:\/\/instagr.am\/p\/*", + "http:\/\/www.instagram.com\/p\/*", + "http:\/\/www.instagr.am\/p\/*", + "https:\/\/instagram.com\/p\/*", + "https:\/\/instagr.am\/p\/*", + "https:\/\/www.instagram.com\/p\/*", + "https:\/\/www.instagr.am\/p\/*" + ], + "url": "https:\/\/api.instagram.com\/oembed", + "formats": [ + "json" + ] + } + ] + }, + { + "provider_name": "Meetup", + "provider_url": "http:\/\/www.meetup.com", + "endpoints": [ + { + "schemes": [ + "http:\/\/meetup.com\/*", + "https:\/\/www.meetup.com\/*", + "https:\/\/meetup.com\/*", + "http:\/\/meetu.ps\/*" + ], + "url": "https:\/\/api.meetup.com\/oembed", + "formats": [ + "json" + ] + } + ] + }, + { + "provider_name": "MixCloud", + "provider_url": "https:\/\/mixcloud.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.mixcloud.com\/*\/*\/", + "https:\/\/www.mixcloud.com\/*\/*\/" + ], + "url": "https:\/\/www.mixcloud.com\/oembed\/" + } + ] + }, + { + "provider_name": "Reddit", + "provider_url": "https:\/\/reddit.com\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/reddit.com\/r\/*\/comments\/*\/*", + "https:\/\/www.reddit.com\/r\/*\/comments\/*\/*" + ], + "url": "https:\/\/www.reddit.com\/oembed" + } + ] + }, + { + "provider_name": "SlideShare", + "provider_url": "http:\/\/www.slideshare.net\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/www.slideshare.net\/*\/*", + "http:\/\/fr.slideshare.net\/*\/*", + "http:\/\/de.slideshare.net\/*\/*", + "http:\/\/es.slideshare.net\/*\/*", + "http:\/\/pt.slideshare.net\/*\/*" + ], + "url": "http:\/\/www.slideshare.net\/api\/oembed\/2", + "discovery": true + } + ] + }, + { + "provider_name": "SoundCloud", + "provider_url": "http:\/\/soundcloud.com\/", + "endpoints": [ + { + "schemes": [ + "http:\/\/soundcloud.com\/*", + "https:\/\/soundcloud.com\/*" + ], + "url": "https:\/\/soundcloud.com\/oembed" + } + ] + }, + { + "provider_name": "Twitch", + "provider_url": "https:\/\/www.twitch.tv", + "endpoints": [ + { + "schemes": [ + "http:\/\/clips.twitch.tv\/*", + "https:\/\/clips.twitch.tv\/*", + "http:\/\/www.twitch.tv\/*", + "https:\/\/www.twitch.tv\/*", + "http:\/\/twitch.tv\/*", + "https:\/\/twitch.tv\/*" + ], + "url": "https:\/\/api.twitch.tv\/v4\/oembed", + "formats": [ + "json" + ] + } + ] + }, + { + "provider_name": "Twitter", + "provider_url": "http:\/\/www.twitter.com\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/twitter.com\/*\/status\/*", + "https:\/\/*.twitter.com\/*\/status\/*" + ], + "url": "https:\/\/publish.twitter.com\/oembed" + } + ] + }, + { + "provider_name": "Vimeo", + "provider_url": "https:\/\/vimeo.com\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/vimeo.com\/*", + "https:\/\/vimeo.com\/album\/*\/video\/*", + "https:\/\/vimeo.com\/channels\/*\/*", + "https:\/\/vimeo.com\/groups\/*\/videos\/*", + "https:\/\/vimeo.com\/ondemand\/*\/*", + "https:\/\/player.vimeo.com\/video\/*" + ], + "url": "https:\/\/vimeo.com\/api\/oembed.{format}", + "discovery": true + } + ] + }, + { + "provider_name": "YouTube", + "provider_url": "https:\/\/www.youtube.com\/", + "endpoints": [ + { + "schemes": [ + "https:\/\/*.youtube.com\/watch*", + "https:\/\/*.youtube.com\/v\/*", + "https:\/\/youtu.be\/*" + ], + "url": "https:\/\/www.youtube.com\/oembed", + "discovery": true + } + ] + } +] \ No newline at end of file diff --git a/backend/src/models/User.js b/backend/src/models/User.js index 6448d7450..86900c3ae 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -105,4 +105,8 @@ module.exports = { target: 'Location', direction: 'out', }, + allowEmbedIframes: { + type: 'boolean', + default: false, + }, } diff --git a/backend/src/schema/resolvers/embeds/findProvider.js b/backend/src/schema/resolvers/embeds/findProvider.js index 491cbb9e8..a4240895f 100644 --- a/backend/src/schema/resolvers/embeds/findProvider.js +++ b/backend/src/schema/resolvers/embeds/findProvider.js @@ -3,6 +3,7 @@ import path from 'path' import minimatch from 'minimatch' let oEmbedProvidersFile = fs.readFileSync(path.join(__dirname, './providers.json'), 'utf8') + // some providers allow a format parameter // we need JSON oEmbedProvidersFile = oEmbedProvidersFile.replace(/\{format\}/g, 'json') diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index b5df8111e..06b25b4fa 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -176,6 +176,7 @@ export default { 'about', 'termsAndConditionsAgreedVersion', 'termsAndConditionsAgreedAt', + 'allowEmbedIframes', ], boolean: { followedByCurrentUser: diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js index 784a48c06..986f4a41f 100644 --- a/backend/src/schema/resolvers/users.spec.js +++ b/backend/src/schema/resolvers/users.spec.js @@ -86,6 +86,7 @@ describe('UpdateUser', () => { name: 'John Doe', termsAndConditionsAgreedVersion: null, termsAndConditionsAgreedAt: null, + allowEmbedIframes: false, } variables = { diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index f1c38b8d6..d9084dd90 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -27,6 +27,8 @@ type User { termsAndConditionsAgreedVersion: String termsAndConditionsAgreedAt: String + allowEmbedIframes: Boolean + friends: [User]! @relation(name: "FRIENDS", direction: "BOTH") friendsCount: Int! @cypher(statement: "MATCH (this)<-[: FRIENDS]->(r: User) RETURN COUNT(DISTINCT r)") @@ -166,6 +168,7 @@ type Mutation { about: String termsAndConditionsAgreedVersion: String termsAndConditionsAgreedAt: String + allowEmbedIframes: Boolean ): User DeleteUser(id: ID!, resource: [Deletable]): User diff --git a/backend/src/seed/factories/users.js b/backend/src/seed/factories/users.js index 962f92781..b65be795d 100644 --- a/backend/src/seed/factories/users.js +++ b/backend/src/seed/factories/users.js @@ -16,6 +16,7 @@ export default function create() { about: faker.lorem.paragraph(), termsAndConditionsAgreedVersion: '0.0.1', termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z', + allowEmbedIframes: false, } defaults.slug = slugify(defaults.name, { lower: true }) args = { diff --git a/webapp/components/Editor/Editor.story.js b/webapp/components/Editor/Editor.story.js index aaaa0eb58..7a69b347f 100644 --- a/webapp/components/Editor/Editor.story.js +++ b/webapp/components/Editor/Editor.story.js @@ -5,8 +5,12 @@ import helpers from '~/storybook/helpers' import Vue from 'vue' const embed = { + image: 'https://i.ytimg.com/vi/ptCcgLM-p8k/maxresdefault_live.jpg', + title: 'Video Titel', + // html: null, + description: 'Video Description', html: - '', + '', } const plugins = [ @@ -114,15 +118,12 @@ storiesOf('Editor', module) }), template: ``, })) - .add('Embeds', () => ({ + .add('Embeds with iframe', () => ({ components: { HcEditor }, store: helpers.store, data: () => ({ users, content: ` -

- The following link should render a youtube video in addition to the link. -

https://www.youtube.com/watch?v=qkdXAtO40Fo @@ -130,3 +131,16 @@ storiesOf('Editor', module) }), template: ``, })) + .add('Embeds with plain link', () => ({ + components: { HcEditor }, + store: helpers.store, + data: () => ({ + users, + content: ` + + https://telegram.org/ + + `, + }), + template: ``, + })) diff --git a/webapp/components/Editor/Editor.vue b/webapp/components/Editor/Editor.vue index 8d3ce281b..fa37c64dc 100644 --- a/webapp/components/Editor/Editor.vue +++ b/webapp/components/Editor/Editor.vue @@ -342,4 +342,84 @@ li > p { margin: 0 0 $space-x-small; } } + +.ProseMirror[contenteditable='false'] { + .embed-close-button { + display: none; + } +} + +.embed-container { + position: relative; + padding: 0; + margin: $space-small auto; + overflow: hidden; + border-radius: $border-radius-base; + border: 1px solid $color-neutral-70; + background-color: $color-neutral-90; +} + +.embed-content { + width: 100%; + height: 100%; + + h4 { + margin: $space-small 0 0 $space-small; + } + + p, + a { + display: block; + margin: 0 0 0 $space-small; + } +} + +.embed-preview-image { + width: 100%; + height: auto; +} + +.embed-preview-image--clickable { + cursor: pointer; +} + +.embed-html { + width: 100%; + + iframe { + width: 100%; + } +} + +.embed-overlay { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + + padding: $space-large; + background-color: $color-neutral-100; +} + +.embed-buttons { + button { + margin-right: $space-small; + } +} + +.embed-checkbox { + display: flex; + + input { + margin-right: $space-small; + } +} + +.embed-close-button { + position: absolute; + top: $space-x-small; + right: $space-x-small; + background-color: rgba(250, 249, 250, 0.6); +} diff --git a/webapp/components/Editor/defaultExtensions.spec.js b/webapp/components/Editor/defaultExtensions.spec.js index 5bf8126b0..78924db55 100644 --- a/webapp/components/Editor/defaultExtensions.spec.js +++ b/webapp/components/Editor/defaultExtensions.spec.js @@ -35,7 +35,7 @@ describe('defaultExtensions', () => { it('renders mentioning as link', () => { const editor = createEditor() const expected = - '

This is a post content mentioning @alicia-luettgen.

' + '

This is a post content mentioning @alicia-luettgen.

' expect(editor.getHTML()).toEqual(expected) }) }) @@ -49,7 +49,7 @@ describe('defaultExtensions', () => { it('renders hashtag as link', () => { const editor = createEditor() const expected = - '

This is a post content with a hashtag #metoo.

' + '

This is a post content with a hashtag #metoo.

' expect(editor.getHTML()).toEqual(expected) }) }) diff --git a/webapp/components/Editor/nodes/Embed.js b/webapp/components/Editor/nodes/Embed.js index 0a12e06ef..0d7a82a18 100644 --- a/webapp/components/Editor/nodes/Embed.js +++ b/webapp/components/Editor/nodes/Embed.js @@ -1,13 +1,12 @@ import { Node } from 'tiptap' import pasteRule from '../commands/pasteRule' import { compileToFunctions } from 'vue-template-compiler' +import Vue from 'vue' +import EmbedComponent from '~/components/Embed/EmbedComponent' + +Vue.component(EmbedComponent) +const template = `` -const template = ` - -
- {{ dataEmbedUrl }} - -` const compiledTemplate = compileToFunctions(template) export default class Embed extends Node { @@ -67,16 +66,13 @@ export default class Embed extends Node { embedData: {}, }), async created() { - if (!this.options) return {} - this.embedData = await this.options.onEmbed({ url: this.dataEmbedUrl }) + if (this.options) { + this.embedData = await this.options.onEmbed({ url: this.dataEmbedUrl }) + } }, computed: { - embedClass() { - return this.embedHtml ? 'embed' : '' - }, - embedHtml() { - const { html = '' } = this.embedData - return html + componentType() { + return EmbedComponent }, dataEmbedUrl: { get() { diff --git a/webapp/components/Editor/nodes/Embed.spec.js b/webapp/components/Editor/nodes/Embed.spec.js index e38bda061..639f99338 100644 --- a/webapp/components/Editor/nodes/Embed.spec.js +++ b/webapp/components/Editor/nodes/Embed.spec.js @@ -1,31 +1,30 @@ -import { shallowMount } from '@vue/test-utils' +import { shallowMount, createLocalVue } from '@vue/test-utils' +import Vuex from 'vuex' +import Styleguide from '@human-connection/styleguide' import Embed from './Embed' -let Wrapper -let propsData +let Wrapper, propsData, component const someUrl = 'https://www.youtube.com/watch?v=qkdXAtO40Fo' +const localVue = createLocalVue() + +localVue.use(Vuex) +localVue.use(Styleguide) describe('Embed.vue', () => { beforeEach(() => { propsData = {} - const component = new Embed() - Wrapper = ({ mocks, propsData }) => { + component = new Embed() + Wrapper = ({ propsData }) => { return shallowMount(component.view, { propsData }) } }) - it('renders anchor', () => { - propsData = { - node: { attrs: { href: someUrl } }, - } - expect(Wrapper({ propsData }).is('a')).toBe(true) - }) - describe('given a href', () => { describe('onEmbed returned embed data', () => { beforeEach(() => { propsData.options = { onEmbed: () => ({ + __typename: 'Embed', type: 'video', title: 'Baby Loves Cat', author: 'Merkley Family', @@ -49,9 +48,7 @@ describe('Embed.vue', () => { propsData.node = { attrs: { href: 'https://www.youtube.com/watch?v=qkdXAtO40Fo' } } const wrapper = Wrapper({ propsData }) await wrapper.html() - expect(wrapper.find('div iframe').attributes('src')).toEqual( - 'https://www.youtube.com/embed/qkdXAtO40Fo?feature=oembed', - ) + expect(wrapper.contains('embed-component-stub')).toBe(true) }) }) diff --git a/webapp/components/Editor/nodes/Link.js b/webapp/components/Editor/nodes/Link.js index 6c015c030..4856566d6 100644 --- a/webapp/components/Editor/nodes/Link.js +++ b/webapp/components/Editor/nodes/Link.js @@ -27,6 +27,7 @@ export default class Link extends TipTapLink { { ...node.attrs, rel: 'noopener noreferrer nofollow', + target: '_blank', }, 0, ], diff --git a/webapp/components/Editor/plugins/eventHandler.js b/webapp/components/Editor/plugins/eventHandler.js index c390a066d..807949aa8 100644 --- a/webapp/components/Editor/plugins/eventHandler.js +++ b/webapp/components/Editor/plugins/eventHandler.js @@ -10,7 +10,6 @@ export default class EventHandler extends Extension { new Plugin({ props: { transformPastedText(text) { - // console.log('#### transformPastedText', text) return text.trim() }, transformPastedHTML(html) { @@ -33,7 +32,6 @@ export default class EventHandler extends Extension { .replace(/

(\s*
\s*)+/gim, '

') // remove additional linebreaks when last child inside p tags .replace(/(\s*
\s*)+<\/p>/gim, '

') - // console.log('#### transformPastedHTML', html) return html }, // transformPasted(slice) { diff --git a/webapp/components/Embed/EmbedComponent.spec.js b/webapp/components/Embed/EmbedComponent.spec.js new file mode 100644 index 000000000..5ad8dd324 --- /dev/null +++ b/webapp/components/Embed/EmbedComponent.spec.js @@ -0,0 +1,206 @@ +import { mount, createLocalVue } from '@vue/test-utils' +import Vuex from 'vuex' +import Styleguide from '@human-connection/styleguide' +import EmbedComponent from './EmbedComponent' + +let wrapper, propsData, getters, mocks +const someUrl = 'https://www.youtube.com/watch?v=qkdXAtO40Fo' +const localVue = createLocalVue() + +localVue.use(Vuex) +localVue.use(Styleguide) + +describe('EmbedComponent.vue', () => { + const Wrapper = () => { + const store = new Vuex.Store({ + getters, + }) + return mount(EmbedComponent, { propsData, localVue, store, mocks }) + } + + beforeEach(() => { + mocks = { + $t: a => a, + $apollo: { + mutate: jest + .fn() + .mockResolvedValueOnce({ data: { UpdateUser: { allowEmbedIframes: true } } }), + }, + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + } + propsData = {} + getters = { + 'auth/user': () => { + return { id: 'u5', allowEmbedIframes: false } + }, + } + }) + + describe('given a href only for a link ', () => { + beforeEach(() => { + propsData.embedData = { + __typename: 'Embed', + type: 'link', + title: '👻 ✉️ Bruno... le ciel sur répondeur ! 🔮 🧠 - Clément FREZE', + author: null, + publisher: 'PeerTube.social', + date: null, + description: + 'Salut tout le monde ! Aujourd’hui, une vidéo sur le scepticisme, nous allons parler médiumnité avec le cas de Bruno CHARVET : « Bruno, un nouveau message ». Merci de rester respectueux dans les commentaires : SOURCES : Les sources des vi...', + url: 'https://peertube.social/videos/watch/f3cb1945-a8f7-481f-a465-946c6f884e50', + image: 'https://peertube.social/static/thumbnails/f3cb1945-a8f7-481f-a465-946c6f884e50.jpg', + audio: null, + video: null, + lang: 'fr', + sources: ['resource', 'oembed'], + html: null, + } + wrapper = Wrapper() + }) + + it('shows the title', () => { + expect(wrapper.find('h4').text()).toBe( + '👻 ✉️ Bruno... le ciel sur répondeur ! 🔮 🧠 - Clément FREZE', + ) + }) + + it('shows the description', () => { + expect(wrapper.find('.embed-content p').text()).toBe( + 'Salut tout le monde ! Aujourd’hui, une vidéo sur le scepticisme, nous allons parler médiumnité avec le cas de Bruno CHARVET : « Bruno, un nouveau message ». Merci de rester respectueux dans les commentaires : SOURCES : Les sources des vi...', + ) + }) + + it('shows preview Images for link', () => { + expect(wrapper.find('.embed-preview-image').exists()).toBe(true) + }) + }) + + describe('given a href with embed html', () => { + describe('onEmbed returned title and description', () => { + beforeEach(() => { + propsData.embedData = { + __typename: 'Embed', + title: 'Baby Loves Cat', + description: + 'She’s incapable of controlling her limbs when her kitty is around. The obsession grows every day. Ps. That’s a sleep sack she’s in. Not a starfish outfit. Al...', + } + wrapper = Wrapper() + }) + + it('show the title', () => { + expect(wrapper.find('h4').text()).toBe('Baby Loves Cat') + }) + + it('show the desciption', () => { + expect(wrapper.find('.embed-content p').text()).toBe( + 'She’s incapable of controlling her limbs when her kitty is around. The obsession grows every day. Ps. That’s a sleep sack she’s in. Not a starfish outfit. Al...', + ) + }) + + describe('onEmbed returned embed data with html', () => { + beforeEach(() => { + propsData.embedData = { + __typename: 'Embed', + type: 'video', + title: 'Baby Loves Cat', + author: 'Merkley Family', + publisher: 'YouTube', + date: '2015-08-16T00:00:00.000Z', + description: + 'She’s incapable of controlling her limbs when her kitty is around. The obsession grows every day. Ps. That’s a sleep sack she’s in. Not a starfish outfit. Al...', + url: someUrl, + image: 'https://i.ytimg.com/vi/qkdXAtO40Fo/maxresdefault.jpg', + audio: null, + video: null, + lang: 'de', + sources: ['resource', 'oembed'], + html: + '', + } + wrapper = Wrapper() + }) + + it('shows a simple link when a user closes the embed preview', () => { + wrapper.find('.embed-close-button').trigger('click') + expect(wrapper.vm.showLinkOnly).toBe(true) + }) + + it('opens the data privacy overlay when a user clicks on the preview image', () => { + wrapper.find('.embed-preview-image--clickable').trigger('click') + expect(wrapper.vm.showOverlay).toBe(true) + }) + + describe('shows iframe', () => { + beforeEach(() => { + wrapper.setData({ showOverlay: true }) + }) + + it('when user agress', () => { + wrapper.find('.ds-button-primary').trigger('click') + expect(wrapper.vm.showEmbed).toBe(true) + }) + + it('does not show iframe when user clicks to cancel', () => { + wrapper.find('.ds-button-ghost').trigger('click') + expect(wrapper.vm.showEmbed).toBe(false) + }) + + describe("doesn't set permanently", () => { + beforeEach(() => { + wrapper.find('.ds-button-primary').trigger('click') + }) + + it("if user doesn't give consent", () => { + expect(wrapper.vm.checkedAlwaysAllowEmbeds).toBe(false) + }) + + it("doesn't update the user's profile", () => { + expect(mocks.$apollo.mutate).not.toHaveBeenCalled() + }) + }) + + describe('sets permanently', () => { + beforeEach(() => { + wrapper.find('input[type=checkbox]').trigger('click') + wrapper.find('.ds-button-primary').trigger('click') + }) + + it('changes setting permanetly when user requests', () => { + expect(wrapper.vm.checkedAlwaysAllowEmbeds).toBe(true) + }) + + it("updates the user's profile", () => { + expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1) + }) + }) + }) + + describe('immediately shows', () => { + beforeEach(() => { + getters = { + 'auth/user': () => { + return { id: 'u5', allowEmbedIframes: true } + }, + } + wrapper = Wrapper() + }) + + it('sets showEmbed to true', () => { + expect(wrapper.vm.showEmbed).toBe(true) + }) + + it('the iframe returned from oEmbed', () => { + expect(wrapper.find('iframe').html()).toEqual(propsData.embedData.html) + }) + + it('does not display image to click', () => { + expect(wrapper.find('.embed-preview-image--clickable').exists()).toBe(false) + }) + }) + }) + }) + }) +}) diff --git a/webapp/components/Embed/EmbedComponent.vue b/webapp/components/Embed/EmbedComponent.vue new file mode 100644 index 000000000..5dc8ad00c --- /dev/null +++ b/webapp/components/Embed/EmbedComponent.vue @@ -0,0 +1,153 @@ + + + diff --git a/webapp/graphql/User.js b/webapp/graphql/User.js index dbc1997af..8e3ff06af 100644 --- a/webapp/graphql/User.js +++ b/webapp/graphql/User.js @@ -135,6 +135,17 @@ export const unfollowUserMutation = i18n => { ` } +export const allowEmbedIframesMutation = () => { + return gql` + mutation($id: ID!, $allowEmbedIframes: Boolean) { + UpdateUser(id: $id, allowEmbedIframes: $allowEmbedIframes) { + id + allowEmbedIframes + } + } + ` +} + export const checkSlugAvailableQuery = gql` query($slug: String!) { User(slug: $slug) { diff --git a/webapp/locales/de.json b/webapp/locales/de.json index c282533b7..b7965062b 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -108,6 +108,12 @@ "noHashtagsFound": "Keine Hashtags gefunden", "addHashtag": "Neuer Hashtag", "addLetter": "Tippe einen Buchstaben" + }, + "embed": { + "data_privacy_warning": "Achte auf deine Daten!", + "data_privacy_info": "Deine Daten wurden noch nicht an Drittanbieter weitergegeben. Wenn du dieses Video jetzt abspielst, registriert der folgende Anbieter wahrscheinlich deine Nutzerdaten:", + "play_now": "Jetzt ansehen", + "always_allow": "Inhalte von Drittanbietern immer anzeigen (diese Einstellung kannst du jederzeit ändern)" } }, "profile": { @@ -337,6 +343,19 @@ "submitted": "Kommentar Gesendet", "updated": "Änderungen gespeichert" }, + "allowEmbeds": { + "name": "Drittanbieter", + "info-description": "Wenn du zustimmst werden in den Beiträgen aus der folgenden Liste an Providern Fremdcode von anderen Anbietern (Drittanbietern) in Form von eingebundenen Videos, Bilder oder Text automatisch eingebunden werden.", + "description": "Du hast zugestimmt das in den Beiträgen aus der folgenden Liste an Providern Fremdcode von anderen Anbietern (Drittanbietern) in Form von eingebundenen Videos, Bilder oder Text automatisch eingebunden werden.", + "statustext": "Momentan ist das automatische einbinden:", + "statuschange": "Einstellung ändern", + "false": "Abgestellt", + "true": "Zugelassen", + "button-tofalse": "Abstellen", + "button-totrue": "dauerhaft zulassen", + "third-party-false": "Es wird kein Service von Drittanbietern automatisch eingebunden.", + "third-party-true": "Das einbinden der Services von Drittanbietern ist dauerhaft zugelassen und gespeichert für komende Sitzungen." + }, "edited": "bearbeitet" }, "comment": { diff --git a/webapp/locales/en.json b/webapp/locales/en.json index f099cdbbc..162f1a0f0 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -109,6 +109,12 @@ "noHashtagsFound": "No hashtags found", "addHashtag": "New hashtag", "addLetter": "Type a letter" + }, + "embed": { + "data_privacy_warning": "Data Privacy Warning!", + "data_privacy_info": "Your data has not yet been shared with any third party providers. If you proceed to watch this video the following provider will likely collect user data:", + "play_now": "Watch now", + "always_allow": "Always allow embedded content by third party providers (this setting can be changed any time)" } }, "profile": { @@ -338,6 +344,19 @@ "submitted": "Comment Submitted", "updated": "Changes Saved" }, + "allowEmbeds": { + "name": "Third party providers", + "info-description": "If you agree, the posts from the following list of providers will automatically include third-party code from other providers (third parties) in the form of embedded videos, images, or text.", + "description": "You have agreed that in the contributions from the following list of providers, foreign code from other providers (third parties) in the form of embedded videos, images or text automatically are embedded.", + "statustext": "At the moment this is automatic embedding:", + "statuschange": "Change setting", + "false": "Turned off", + "true": "Admitted", + "button-tofalse": "turn-off", + "button-totrue": "allow permanently", + "third-party-false": "It automatically integrates no third-party providers' service.", + "third-party-true": "The inclusion of third-party services is permanently allowed and stored for future sessions." + }, "edited": "edited" }, "comment": { diff --git a/webapp/middleware/termsAndConditions.js b/webapp/middleware/termsAndConditions.js index 64141eed0..68ad49bf8 100644 --- a/webapp/middleware/termsAndConditions.js +++ b/webapp/middleware/termsAndConditions.js @@ -6,6 +6,7 @@ export default async ({ store, env, route, redirect }) => { if (publicPages.indexOf(route.name) >= 0) { return true } + if (route.name === 'terms-and-conditions-confirm') return true // avoid endless loop if (store.getters['auth/termsAndConditionsAgreed']) return true diff --git a/webapp/package.json b/webapp/package.json index 17fd9403e..952913e4b 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -133,4 +133,4 @@ "vue-svg-loader": "~0.12.0", "vue-template-compiler": "^2.6.10" } -} +} \ No newline at end of file diff --git a/webapp/pages/settings.vue b/webapp/pages/settings.vue index 5795792f9..a7604ea5f 100644 --- a/webapp/pages/settings.vue +++ b/webapp/pages/settings.vue @@ -39,6 +39,10 @@ export default { name: this.$t('settings.blocked-users.name'), path: `/settings/blocked-users`, }, + { + name: this.$t('post.allowEmbeds.name'), + path: `/settings/allow-embeds`, + }, { name: this.$t('settings.deleteUserAccount.name'), path: `/settings/delete-account`, diff --git a/webapp/pages/settings/allow-embeds.vue b/webapp/pages/settings/allow-embeds.vue new file mode 100644 index 000000000..c76a50422 --- /dev/null +++ b/webapp/pages/settings/allow-embeds.vue @@ -0,0 +1,117 @@ + + + diff --git a/webapp/store/auth.js b/webapp/store/auth.js index 498477660..90c59a8f5 100644 --- a/webapp/store/auth.js +++ b/webapp/store/auth.js @@ -86,6 +86,7 @@ export const actions = { locationName contributionsCount commentedCount + allowEmbedIframes termsAndConditionsAgreedVersion socialMedia { id