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;
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
export const EventBus = new Vue();
|
||||
|
Loading…
Reference in New Issue