URL previews
This commit is contained in:
		
							parent
							
								
									8115340ddd
								
							
						
					
					
						commit
						0107dd60a1
					
				| @ -4,7 +4,7 @@ const urlFormat = '((?:https?|ftps?|irc)://[^\\s/$.?#"\']+\\.[^\\s"]+)'; | ||||
| export const findUrlRegex = new RegExp(`(\\[url[=\\]]\\s*)?${urlFormat}`, 'gi'); | ||||
| export const urlRegex = new RegExp(`^${urlFormat}$`); | ||||
| 
 | ||||
| function domain(url: string): string | undefined { | ||||
| export function domain(url: string): string | undefined { | ||||
|     const pieces = urlRegex.exec(url); | ||||
|     if(pieces === null) return; | ||||
|     const match = pieces[1].match(/(?:(https?|ftps?|irc):)?\/\/(?:www.)?([^\/]+)/); | ||||
| @ -17,6 +17,39 @@ function fixURL(url: string): string { | ||||
|     return url.replace(/ /g, '%20'); | ||||
| } | ||||
| 
 | ||||
| export function analyzeUrlTag(parser: BBCodeParser, param: string, content: string): {success: boolean, url?: string, domain?: string, textContent: string} { | ||||
|     let url: string | undefined, textContent: string = content; | ||||
|     let success = true; | ||||
| 
 | ||||
|     if(param.length > 0) { | ||||
|         url = param.trim(); | ||||
|         if(content.length === 0) textContent = param; | ||||
|     } else if(content.length > 0) url = content; | ||||
|     else { | ||||
|         parser.warning('url tag contains no url.'); | ||||
|         textContent = ''; | ||||
|         success = false; | ||||
|     } | ||||
| 
 | ||||
|     if((success) && (url)) { | ||||
|         // This fixes problems where content based urls are marked as invalid if they contain spaces.
 | ||||
|         url = fixURL(url); | ||||
| 
 | ||||
|         if (!urlRegex.test(url)) { | ||||
|             textContent = `[BAD URL] ${url}`; | ||||
|             success = false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|         success, | ||||
|         url, | ||||
|         textContent, | ||||
|         domain: url ? domain(url) : undefined | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| export class CoreBBCodeParser extends BBCodeParser { | ||||
|     /*tslint:disable-next-line:typedef*///https://github.com/palantir/tslint/issues/711
 | ||||
|     constructor(public makeLinksClickable = true) { | ||||
| @ -40,41 +73,32 @@ export class CoreBBCodeParser extends BBCodeParser { | ||||
|             return el; | ||||
|         })); | ||||
|         this.addTag(new BBCodeTextTag('url', (parser, parent, param, content) => { | ||||
|             const tagData = analyzeUrlTag(parser, param, content); | ||||
|             const element = parser.createElement('span'); | ||||
| 
 | ||||
|             parent.appendChild(element); | ||||
| 
 | ||||
|             let url: string, display: string = content; | ||||
|             if(param.length > 0) { | ||||
|                 url = param.trim(); | ||||
|                 if(content.length === 0) display = param; | ||||
|             } else if(content.length > 0) url = content; | ||||
|             else { | ||||
|                 parser.warning('url tag contains no url.'); | ||||
|                 element.textContent = ''; | ||||
|             if (!tagData.success) { | ||||
|                 element.textContent = tagData.textContent; | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // This fixes problems where content based urls are marked as invalid if they contain spaces.
 | ||||
|             url = fixURL(url); | ||||
|             if(!urlRegex.test(url)) { | ||||
|                 element.textContent = `[BAD URL] ${url}`; | ||||
|                 return; | ||||
|             } | ||||
|             const fa = parser.createElement('i'); | ||||
|             fa.className = 'fa fa-link'; | ||||
|             element.appendChild(fa); | ||||
|             const a = parser.createElement('a'); | ||||
|             a.href = url; | ||||
|             a.href = tagData.url as string; | ||||
|             a.rel = 'nofollow noreferrer noopener'; | ||||
|             a.target = '_blank'; | ||||
|             a.className = 'user-link'; | ||||
|             a.title = url; | ||||
|             a.textContent = display; | ||||
|             a.title = tagData.url as string; | ||||
|             a.textContent = tagData.textContent; | ||||
|             element.appendChild(a); | ||||
|             const span = document.createElement('span'); | ||||
|             span.className = 'link-domain bbcode-pseudo'; | ||||
|             span.textContent = ` [${domain(url)}]`; | ||||
|             span.textContent = ` [${tagData.domain}]`; | ||||
|             element.appendChild(span); | ||||
| 
 | ||||
|             return element; | ||||
|         })); | ||||
|     } | ||||
|  | ||||
| @ -92,6 +92,10 @@ export class BBCodeParser { | ||||
|         this._tags[impl.tag] = impl; | ||||
|     } | ||||
| 
 | ||||
|     getTag(tag: string): BBCodeTag | undefined { | ||||
|         return this._tags[tag]; | ||||
|     } | ||||
| 
 | ||||
|     removeTag(tag: string): void { | ||||
|         delete this._tags[tag]; | ||||
|     } | ||||
|  | ||||
| @ -136,6 +136,7 @@ | ||||
|         <settings ref="settingsDialog" :conversation="conversation"></settings> | ||||
|         <logs ref="logsDialog" :conversation="conversation"></logs> | ||||
|         <manage-channel ref="manageDialog" v-if="isChannel(conversation)" :channel="conversation.channel"></manage-channel> | ||||
|         <image-preview ref="imagePreview"></image-preview> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| @ -150,6 +151,7 @@ | ||||
|     import { characterImage, getByteLength, getKey } from "./common"; | ||||
|     import ConversationSettings from './ConversationSettings.vue'; | ||||
|     import core from './core'; | ||||
|     import ImagePreview from './ImagePreview.vue'; | ||||
|     import {Channel, channelModes, Character, Conversation, Settings} from './interfaces'; | ||||
|     import l from './localize'; | ||||
|     import Logs from './Logs.vue'; | ||||
| @ -162,7 +164,8 @@ | ||||
|     @Component({ | ||||
|         components: { | ||||
|             user: UserView, 'bbcode-editor': Editor, 'manage-channel': ManageChannel, settings: ConversationSettings, | ||||
|             logs: Logs, 'message-view': MessageView, bbcode: BBCodeView, 'command-help': CommandHelp | ||||
|             logs: Logs, 'message-view': MessageView, bbcode: BBCodeView, 'command-help': CommandHelp, | ||||
|             'image-preview': ImagePreview | ||||
|         } | ||||
|     }) | ||||
|     export default class ConversationView extends Vue { | ||||
| @ -471,6 +474,7 @@ | ||||
|             updateAutoPostingState(); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         hasSFC(message: Conversation.Message): message is Conversation.SFCMessage { | ||||
|             return (<Partial<Conversation.SFCMessage>>message).sfc !== undefined; | ||||
|         } | ||||
|  | ||||
							
								
								
									
										132
									
								
								chat/ImagePreview.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								chat/ImagePreview.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,132 @@ | ||||
| <template> | ||||
|     <div class="image-preview-wrapper" v-if="isVisible()"> | ||||
|         <webview class="image-preview-external" :src="isExternalUrl() ? url : null" :style="{display: isExternalUrl() ? 'flex' : 'none'}"></webview> | ||||
|         <div | ||||
|             class="image-preview-local" | ||||
|             :style="{backgroundImage: `url(${url})`, display: isInternalUrl() ? 'block' : 'none'}" | ||||
|         > | ||||
|         </div> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|     import {Component, Hook} from '@f-list/vue-ts'; | ||||
|     import Vue from 'vue'; | ||||
|     import {EventBus} from './event-bus'; | ||||
|     import {domain } from '../bbcode/core'; | ||||
| 
 | ||||
|     @Component | ||||
|     export default class ImagePreview extends Vue { | ||||
|         public visible: boolean = false; | ||||
|         public url: string|null = null; | ||||
|         public domain: string|undefined; | ||||
| 
 | ||||
|         private interval: any = null; | ||||
| 
 | ||||
| 
 | ||||
|         @Hook('mounted') | ||||
|         onMounted() { | ||||
|             EventBus.$on( | ||||
|                 'imagepreview-dismiss', | ||||
|                 (eventData: any) => { | ||||
|                     console.log('DIMSMISS'); | ||||
|                     this.dismiss(eventData.url); | ||||
|                 } | ||||
|             ); | ||||
| 
 | ||||
|             EventBus.$on( | ||||
|                 'imagepreview-show', | ||||
|                 (eventData: any) => { | ||||
|                     console.log('SHOW'); | ||||
|                     this.show(eventData.url); | ||||
|                 } | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         dismiss(url: string) { | ||||
|             if (this.url !== url) { | ||||
|                 // simply ignore | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             this.url = null; | ||||
|             this.visible = false; | ||||
| 
 | ||||
|             this.cancelTimer(); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         show(url: string) { | ||||
|             this.url = url; | ||||
|             this.domain = domain(url); | ||||
| 
 | ||||
|             this.cancelTimer(); | ||||
| 
 | ||||
|             this.interval = setTimeout( | ||||
|                 () => { | ||||
|                     this.visible = true; | ||||
|                 }, | ||||
|                 100 | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         cancelTimer() { | ||||
|             if (this.interval) { | ||||
|                 clearTimeout(this.interval); | ||||
|             } | ||||
| 
 | ||||
|             this.interval = null; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         isVisible() { | ||||
|             return this.visible; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         getUrl() { | ||||
|             return this.url; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         isExternalUrl() { | ||||
|             return !((this.domain === 'f-list.net') || (this.domain === 'static.f-list.net')); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         isInternalUrl() { | ||||
|             return !this.isExternalUrl(); | ||||
|         } | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <style lang="scss"> | ||||
|     @import "~bootstrap/scss/functions"; | ||||
|     @import "~bootstrap/scss/variables"; | ||||
|     @import "~bootstrap/scss/mixins/breakpoints"; | ||||
| 
 | ||||
|     .image-preview-external { | ||||
|         position: absolute; | ||||
|         width: 50%; | ||||
|         height: 50%; | ||||
|         top: 0; | ||||
|         left: 0; | ||||
|         pointer-events: none; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     .image-preview-local { | ||||
|         position: absolute; | ||||
|         width: 50%; | ||||
|         height: 50%; | ||||
|         top: 0; | ||||
|         left: 0; | ||||
|         pointer-events: none; | ||||
|         background-size: contain; | ||||
|         background-position: top left; | ||||
|         background-repeat: no-repeat; | ||||
|     } | ||||
| </style> | ||||
							
								
								
									
										58
									
								
								chat/UrlTagView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								chat/UrlTagView.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | ||||
| <template> | ||||
|     <span | ||||
|         @mouseover="show()" | ||||
|         @mouseleave="dismiss()" | ||||
|     > | ||||
|         <i class="fa fa-link"></i> | ||||
|         <a | ||||
|             :href="url" | ||||
|             rel="nofollow noreferrer noopener" | ||||
|             target="_blank" | ||||
|             class="user-link" | ||||
|             :title="url" | ||||
|         >{{text}}</a> | ||||
|         <span | ||||
|             class="link-domain bbcode-pseudo" | ||||
|         > [{{domain}}]</span> | ||||
|     </span> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|     import {Component, Hook, Prop} from '@f-list/vue-ts'; | ||||
|     import Vue from 'vue'; | ||||
|     import {EventBus} from './event-bus'; | ||||
|     // import core from './core'; | ||||
| 
 | ||||
|     @Component | ||||
|     export default class UrlTagView extends Vue { | ||||
|         @Prop({required: true}) | ||||
|         readonly url!: string; | ||||
| 
 | ||||
|         @Prop({required: true}) | ||||
|         readonly text!: string; | ||||
| 
 | ||||
|         @Prop({required: true}) | ||||
|         readonly domain!: string; | ||||
| 
 | ||||
|         @Prop() | ||||
|         hover!: boolean = false; | ||||
| 
 | ||||
|         @Hook("beforeDestroy") | ||||
|         beforeDestroy() { | ||||
|             this.dismiss(); | ||||
|         } | ||||
| 
 | ||||
|         @Hook("deactivated") | ||||
|         deactivate() { | ||||
|             this.dismiss(); | ||||
|         } | ||||
| 
 | ||||
|         dismiss() { | ||||
|             EventBus.$emit('imagepreview-dismiss', {url: this.url}); | ||||
|         } | ||||
| 
 | ||||
|         show() { | ||||
|             EventBus.$emit('imagepreview-show', {url: this.url}); | ||||
|         } | ||||
|     } | ||||
| </script> | ||||
| @ -1,5 +1,5 @@ | ||||
| import Vue, {Component, CreateElement, RenderContext, VNode} from 'vue'; | ||||
| import {CoreBBCodeParser} from '../bbcode/core'; | ||||
| import { CoreBBCodeParser, analyzeUrlTag } from '../bbcode/core'; | ||||
| //tslint:disable-next-line:match-default-export-name
 | ||||
| import BaseEditor from '../bbcode/Editor.vue'; | ||||
| import {BBCodeTextTag} from '../bbcode/parser'; | ||||
| @ -7,6 +7,7 @@ import ChannelView from './ChannelTagView.vue'; | ||||
| import {characterImage} from './common'; | ||||
| import core from './core'; | ||||
| import {Character} from './interfaces'; | ||||
| import UrlView from './UrlTagView.vue'; | ||||
| import UserView from './user_view'; | ||||
| 
 | ||||
| export const BBCodeView: Component = { | ||||
| @ -100,6 +101,27 @@ export default class BBCodeParser extends CoreBBCodeParser { | ||||
|             this.cleanup.push(view); | ||||
|             return root; | ||||
|         })); | ||||
| 
 | ||||
|         this.addTag(new BBCodeTextTag( | ||||
|             'url', | ||||
|             (parser, parent, _, content) => { | ||||
|                 const tagData = analyzeUrlTag(parser, _, content); | ||||
| 
 | ||||
|                 const root = parser.createElement('span'); | ||||
|                 // const el = parser.createElement('span');
 | ||||
|                 parent.appendChild(root); | ||||
|                 // root.appendChild(el);
 | ||||
| 
 | ||||
|                 if (!tagData.success) { | ||||
|                     root.textContent = tagData.textContent; | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 const view = new UrlView({el: root, propsData: {url: tagData.url, text: tagData.textContent, domain: tagData.domain}}); | ||||
|                 this.cleanup.push(view); | ||||
| 
 | ||||
|                 return root; | ||||
|             })); | ||||
|     } | ||||
| 
 | ||||
|     parseEverything(input: string): BBCodeElement { | ||||
|  | ||||
							
								
								
									
										3
									
								
								chat/event-bus.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								chat/event-bus.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| import Vue from 'vue'; | ||||
| export const EventBus = new Vue(); | ||||
| 
 | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user