hardlounge/client/components/LinkPreview.vue

330 lines
7.2 KiB
Vue
Raw Normal View History

2018-07-10 11:57:11 +00:00
<template>
2019-11-25 19:37:51 +00:00
<div
v-if="link.shown"
v-show="link.sourceLoaded || link.type === 'link'"
ref="container"
class="preview"
dir="ltr"
>
2018-07-12 08:26:12 +00:00
<div
ref="content"
2019-07-17 09:33:59 +00:00
:class="['toggle-content', 'toggle-type-' + link.type, {opened: isContentShown}]"
>
2018-07-10 11:57:11 +00:00
<template v-if="link.type === 'link'">
2018-07-10 12:04:25 +00:00
<a
v-if="link.thumb"
2019-11-25 19:37:51 +00:00
v-show="link.sourceLoaded"
2018-07-10 12:04:25 +00:00
:href="link.link"
class="toggle-thumbnail"
target="_blank"
rel="noopener"
2019-09-20 18:10:48 +00:00
@click="onThumbnailClick"
>
2018-07-10 12:04:25 +00:00
<img
:src="link.thumb"
decoding="async"
alt=""
2018-07-10 14:44:50 +00:00
class="thumb"
@error="onThumbnailError"
@abort="onThumbnailError"
@load="onPreviewReady"
2019-07-17 09:33:59 +00:00
/>
2018-07-10 11:57:11 +00:00
</a>
<div class="toggle-text" dir="auto">
2018-07-10 11:57:11 +00:00
<div class="head">
<div class="overflowable">
2018-07-10 12:04:25 +00:00
<a
:href="link.link"
:title="link.head"
target="_blank"
rel="noopener"
2019-07-17 09:33:59 +00:00
>{{ link.head }}</a
>
2018-07-10 11:57:11 +00:00
</div>
2018-07-10 12:04:25 +00:00
<button
2018-07-12 08:26:12 +00:00
v-if="showMoreButton"
:aria-expanded="isContentShown"
:aria-label="moreButtonLabel"
dir="auto"
2018-07-10 12:04:25 +00:00
class="more"
@click="onMoreClick"
2019-07-17 09:33:59 +00:00
>
<span class="more-caret" />
</button>
2018-07-10 11:57:11 +00:00
</div>
<div class="body overflowable">
2019-07-17 09:33:59 +00:00
<a :href="link.link" :title="link.body" target="_blank" rel="noopener">{{
link.body
}}</a>
2018-07-10 11:57:11 +00:00
</div>
</div>
</template>
<template v-else-if="link.type === 'image'">
2019-09-20 18:10:48 +00:00
<a
:href="link.link"
class="toggle-thumbnail"
target="_blank"
rel="noopener"
@click="onThumbnailClick"
>
2019-11-25 19:37:51 +00:00
<img
v-show="link.sourceLoaded"
:src="link.thumb"
decoding="async"
alt=""
@load="onPreviewReady"
/>
2018-07-10 11:57:11 +00:00
</a>
</template>
<template v-else-if="link.type === 'video'">
2019-11-25 19:37:51 +00:00
<video
v-show="link.sourceLoaded"
preload="metadata"
controls
@canplay="onPreviewReady"
>
2019-07-17 09:33:59 +00:00
<source :src="link.media" :type="link.mediaType" />
2018-07-10 11:57:11 +00:00
</video>
</template>
<template v-else-if="link.type === 'audio'">
2019-11-25 19:37:51 +00:00
<audio
v-show="link.sourceLoaded"
controls
preload="metadata"
@canplay="onPreviewReady"
>
2019-07-17 09:33:59 +00:00
<source :src="link.media" :type="link.mediaType" />
2018-07-10 11:57:11 +00:00
</audio>
</template>
<template v-else-if="link.type === 'error'">
<em v-if="link.error === 'image-too-big'">
This image is larger than {{ imageMaxSize }} and cannot be previewed.
2019-07-17 09:33:59 +00:00
<a :href="link.link" target="_blank" rel="noopener">Click here</a>
2018-07-10 11:57:11 +00:00
to open it in a new window.
</em>
<template v-else-if="link.error === 'message'">
<div>
<em>
A preview could not be loaded.
2019-07-17 09:33:59 +00:00
<a :href="link.link" target="_blank" rel="noopener">Click here</a>
2018-07-10 11:57:11 +00:00
to open it in a new window.
</em>
2019-07-17 09:33:59 +00:00
<br />
2018-07-10 12:04:25 +00:00
<pre class="prefetch-error">{{ link.message }}</pre>
2018-07-10 11:57:11 +00:00
</div>
2018-07-10 12:04:25 +00:00
<button
2018-07-12 08:26:12 +00:00
:aria-expanded="isContentShown"
:aria-label="moreButtonLabel"
2018-07-10 12:04:25 +00:00
class="more"
@click="onMoreClick"
2019-07-17 09:33:59 +00:00
>
<span class="more-caret" />
</button>
2018-07-10 11:57:11 +00:00
</template>
</template>
</div>
</div>
</template>
<script lang="ts">
import {
computed,
defineComponent,
inject,
nextTick,
onBeforeUnmount,
onMounted,
onUnmounted,
PropType,
ref,
watch,
} from "vue";
import {onBeforeRouteUpdate} from "vue-router";
2020-03-16 17:58:40 +00:00
import eventbus from "../js/eventbus";
2019-11-16 17:24:03 +00:00
import friendlysize from "../js/helpers/friendlysize";
import {useStore} from "../js/store";
import type {ClientChan, ClientLinkPreview} from "../js/types";
import {imageViewerKey} from "./App.vue";
export default defineComponent({
2018-07-10 11:57:11 +00:00
name: "LinkPreview",
props: {
link: {
type: Object as PropType<ClientLinkPreview>,
required: true,
2018-07-12 08:26:12 +00:00
},
keepScrollPosition: {
type: Function as PropType<() => void>,
required: true,
},
channel: {type: Object as PropType<ClientChan>, required: true},
},
setup(props) {
const store = useStore();
const showMoreButton = ref(false);
const isContentShown = ref(false);
const imageViewer = inject(imageViewerKey);
onBeforeRouteUpdate((to, from, next) => {
// cancel the navigation if the user is trying to close the image viewer
if (imageViewer?.value?.link) {
imageViewer.value.closeViewer();
return next(false);
}
next();
});
const content = ref<HTMLDivElement | null>(null);
const container = ref<HTMLDivElement | null>(null);
const moreButtonLabel = computed(() => {
return isContentShown.value ? "Less" : "More";
});
const imageMaxSize = computed(() => {
if (!props.link.maxSize) {
2019-11-25 19:37:51 +00:00
return;
}
return friendlysize(props.link.maxSize);
});
const handleResize = () => {
nextTick(() => {
if (!content.value || !container.value) {
return;
}
showMoreButton.value = content.value.offsetWidth >= container.value.offsetWidth;
}).catch((e) => {
// eslint-disable-next-line no-console
console.error("Error in LinkPreview.handleResize", e);
});
};
const onPreviewReady = () => {
props.link.sourceLoaded = true;
props.keepScrollPosition();
if (props.link.type === "link") {
handleResize();
}
};
const onPreviewUpdate = () => {
// Don't display previews while they are loading on the server
if (props.link.type === "loading") {
return;
}
2019-11-25 19:37:51 +00:00
// Error does not have any media to render
if (props.link.type === "error") {
onPreviewReady();
}
// If link doesn't have a thumbnail, render it
if (props.link.type === "link") {
handleResize();
props.keepScrollPosition();
}
};
const onThumbnailError = () => {
// If thumbnail fails to load, hide it and show the preview without it
props.link.thumb = "";
onPreviewReady();
};
const onThumbnailClick = (e: MouseEvent) => {
2019-09-20 18:10:48 +00:00
e.preventDefault();
if (!imageViewer?.value) {
return;
}
imageViewer.value.channel = props.channel;
imageViewer.value.link = props.link;
};
const onMoreClick = () => {
isContentShown.value = !isContentShown.value;
props.keepScrollPosition();
};
const updateShownState = () => {
// User has manually toggled the preview, do not apply default
if (props.link.shown !== null) {
return;
}
let defaultState = false;
switch (props.link.type) {
2019-07-17 09:33:59 +00:00
case "error":
2019-12-23 10:15:23 +00:00
// Collapse all errors by default unless its a message about image being too big
if (props.link.error === "image-too-big") {
defaultState = store.state.settings.media;
}
2019-07-17 09:33:59 +00:00
break;
case "link":
defaultState = store.state.settings.links;
2019-07-17 09:33:59 +00:00
break;
default:
defaultState = store.state.settings.media;
}
props.link.shown = defaultState;
};
updateShownState();
watch(
() => props.link.type,
() => {
updateShownState();
onPreviewUpdate();
}
);
onMounted(() => {
eventbus.on("resize", handleResize);
onPreviewUpdate();
});
onBeforeUnmount(() => {
eventbus.off("resize", handleResize);
});
onUnmounted(() => {
// Let this preview go through load/canplay events again,
// Otherwise the browser can cause a resize on video elements
props.link.sourceLoaded = false;
});
return {
moreButtonLabel,
imageMaxSize,
onThumbnailClick,
onThumbnailError,
onMoreClick,
onPreviewReady,
onPreviewUpdate,
showMoreButton,
isContentShown,
content,
container,
};
2018-07-10 14:44:50 +00:00
},
});
2018-07-10 11:57:11 +00:00
</script>