URL previews

This commit is contained in:
Mr. Stallion 2019-06-07 21:26:01 -05:00
parent 8115340ddd
commit 0107dd60a1
7 changed files with 268 additions and 21 deletions

View File

@ -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;
}));
}

View File

@ -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];
}

View File

@ -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
View 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
View 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>

View File

@ -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
View File

@ -0,0 +1,3 @@
import Vue from 'vue';
export const EventBus = new Vue();