mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
add embed, media utils, time utils, url utils
This commit is contained in:
@@ -7,7 +7,7 @@ interface EmbedProps {
|
||||
const Embed = ({ url }: EmbedProps) => {
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
if (youtubeHosts.has(parsedUrl.host)) {
|
||||
if (youtubeHosts.has(parsedUrl.host) || parsedUrl.host.startsWith('yt.')) {
|
||||
return <YoutubeEmbed parsedUrl={parsedUrl} />;
|
||||
}
|
||||
if (xHosts.has(parsedUrl.host)) {
|
||||
@@ -46,23 +46,26 @@ interface EmbedComponentProps {
|
||||
const youtubeHosts = new Set<string>(['youtube.com', 'www.youtube.com', 'youtu.be', 'www.youtu.be', 'm.youtube.com']);
|
||||
|
||||
const YoutubeEmbed = ({ parsedUrl }: EmbedComponentProps) => {
|
||||
let youtubeId;
|
||||
if (parsedUrl.host.endsWith('youtu.be')) {
|
||||
youtubeId = parsedUrl.pathname.replaceAll('/', '');
|
||||
} else {
|
||||
youtubeId = parsedUrl.searchParams.get('v');
|
||||
let youtubeId = parsedUrl.searchParams.get('v');
|
||||
|
||||
if (!youtubeId && parsedUrl.host.includes('youtu.be')) {
|
||||
youtubeId = parsedUrl.pathname.substring(1);
|
||||
}
|
||||
return (
|
||||
<iframe
|
||||
className={styles.videoEmbed}
|
||||
height='100%'
|
||||
width='100%'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowFullScreen
|
||||
title={parsedUrl.href}
|
||||
src={`https://www.youtube-nocookie.com/embed/${youtubeId}`}
|
||||
/>
|
||||
);
|
||||
|
||||
if (youtubeId) {
|
||||
return (
|
||||
<iframe
|
||||
className={styles.videoEmbed}
|
||||
height='100%'
|
||||
width='100%'
|
||||
allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
|
||||
allowFullScreen
|
||||
title={parsedUrl.href}
|
||||
src={`https://www.youtube-nocookie.com/embed/${youtubeId}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const xHosts = new Set<string>(['twitter.com', 'www.twitter.com', 'x.com', 'www.x.com']);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Comment } from '@plebbit/plebbit-react-hooks';
|
||||
import extName from 'ext-name';
|
||||
import { canEmbed } from '../../components/embed';
|
||||
import memoize from 'memoizee';
|
||||
import { isValidURL } from './url-utils';
|
||||
|
||||
export interface CommentMediaInfo {
|
||||
url: string;
|
||||
@@ -16,106 +17,84 @@ export const getHasThumbnail = (commentMediaInfo: CommentMediaInfo | undefined,
|
||||
commentMediaInfo &&
|
||||
(commentMediaInfo.type === 'image' ||
|
||||
commentMediaInfo.type === 'video' ||
|
||||
commentMediaInfo.type === 'gif' ||
|
||||
(commentMediaInfo.type === 'webpage' && commentMediaInfo.thumbnail) ||
|
||||
(commentMediaInfo.type === 'iframe' && iframeThumbnail))
|
||||
? true
|
||||
: false;
|
||||
};
|
||||
|
||||
const getCommentMediaInfo = (comment: Comment) => {
|
||||
const getYouTubeVideoId = (url: URL): string | null => {
|
||||
if (url.host.includes('youtu.be')) {
|
||||
return url.pathname.slice(1);
|
||||
} else if (url.searchParams.has('v')) {
|
||||
return url.searchParams.get('v');
|
||||
} else if (url.host.startsWith('yt.')) {
|
||||
return url.searchParams.get('v');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getPatternThumbnailUrl = (url: URL): string | undefined => {
|
||||
const videoId = getYouTubeVideoId(url);
|
||||
if (videoId) {
|
||||
return `https://img.youtube.com/vi/${videoId}/0.jpg`;
|
||||
}
|
||||
if (url.host.includes('streamable.com')) {
|
||||
const videoId = url.pathname.split('/')[1];
|
||||
return `https://cdn-cf-east.streamable.com/image/${videoId}.jpg`;
|
||||
}
|
||||
};
|
||||
|
||||
const getLinkMediaInfo = (link: string): CommentMediaInfo | undefined => {
|
||||
if (!isValidURL(link)) {
|
||||
return;
|
||||
}
|
||||
const url = new URL(link);
|
||||
let patternThumbnailUrl: string | undefined;
|
||||
let type: string = 'webpage';
|
||||
let mime: string | undefined;
|
||||
|
||||
// Check for common dynamic image URL patterns
|
||||
if (link.includes('/_next/image?')) {
|
||||
// Next.js Image component
|
||||
return { url: link, type: 'image' };
|
||||
}
|
||||
|
||||
try {
|
||||
mime = extName(url.pathname.slice(url.pathname.lastIndexOf('/') + 1))[0]?.mime;
|
||||
if (mime) {
|
||||
if (mime.startsWith('image')) {
|
||||
type = mime === 'image/gif' ? 'gif' : 'image';
|
||||
} else if (mime.startsWith('video')) {
|
||||
type = 'video';
|
||||
} else if (mime.startsWith('audio')) {
|
||||
type = 'audio';
|
||||
}
|
||||
}
|
||||
|
||||
if (canEmbed(url) || url.host.startsWith('yt.')) {
|
||||
type = 'iframe';
|
||||
patternThumbnailUrl = getPatternThumbnailUrl(url);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return { url: link, type, patternThumbnailUrl };
|
||||
};
|
||||
|
||||
const getCommentMediaInfo = (comment: Comment): CommentMediaInfo | undefined => {
|
||||
if (!comment?.thumbnailUrl && !comment?.link) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (comment?.link) {
|
||||
let mime: string | undefined;
|
||||
try {
|
||||
mime = extName(new URL(comment?.link).pathname.toLowerCase().replace('/', ''))[0]?.mime;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(comment.link);
|
||||
const host = url.hostname;
|
||||
let patternThumbnailUrl;
|
||||
|
||||
if (['youtube.com', 'www.youtube.com', 'youtu.be', 'www.youtu.be', 'm.youtube.com'].includes(host)) {
|
||||
const videoId = host === 'youtu.be' ? url.pathname.slice(1) : url.searchParams.get('v');
|
||||
patternThumbnailUrl = `https://img.youtube.com/vi/${videoId}/0.jpg`;
|
||||
} else if (host.includes('streamable.com')) {
|
||||
const videoId = url.pathname.split('/')[1];
|
||||
patternThumbnailUrl = `https://cdn-cf-east.streamable.com/image/${videoId}.jpg`;
|
||||
}
|
||||
|
||||
if (canEmbed(url)) {
|
||||
return {
|
||||
url: comment.link,
|
||||
type: 'iframe',
|
||||
thumbnail: comment.thumbnailUrl,
|
||||
patternThumbnailUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (mime?.startsWith('image')) {
|
||||
return { url: comment.link, type: 'image' };
|
||||
}
|
||||
if (mime?.startsWith('video')) {
|
||||
return { url: comment.link, type: 'video', thumbnail: comment.thumbnailUrl };
|
||||
}
|
||||
if (mime?.startsWith('audio')) {
|
||||
return { url: comment.link, type: 'audio' };
|
||||
}
|
||||
|
||||
if (comment?.thumbnailUrl && comment?.thumbnailUrl !== comment?.link) {
|
||||
return { url: comment.link, type: 'webpage', thumbnail: comment.thumbnailUrl };
|
||||
}
|
||||
|
||||
if (comment?.link) {
|
||||
return { url: comment.link, type: 'webpage' };
|
||||
}
|
||||
const linkInfo = comment.link ? getLinkMediaInfo(comment.link) : undefined;
|
||||
if (linkInfo) {
|
||||
linkInfo.thumbnail = comment.thumbnailUrl || linkInfo.thumbnail;
|
||||
return linkInfo;
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
export const getCommentMediaInfoMemoized = memoize(getCommentMediaInfo, { max: 1000 });
|
||||
|
||||
const getLinkMediaInfo = (link: string) => {
|
||||
let mime: string | undefined;
|
||||
try {
|
||||
mime = extName(new URL(link).pathname.toLowerCase().replace('/', ''))[0]?.mime;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(link);
|
||||
const host = url.hostname;
|
||||
let patternThumbnailUrl;
|
||||
|
||||
if (['youtube.com', 'www.youtube.com', 'youtu.be', 'www.youtu.be', 'm.youtube.com'].includes(host)) {
|
||||
const videoId = host === 'youtu.be' ? url.pathname.slice(1) : url.searchParams.get('v');
|
||||
patternThumbnailUrl = `https://img.youtube.com/vi/${videoId}/0.jpg`;
|
||||
} else if (host.includes('streamable.com')) {
|
||||
const videoId = url.pathname.split('/')[1];
|
||||
patternThumbnailUrl = `https://cdn-cf-east.streamable.com/image/${videoId}.jpg`;
|
||||
}
|
||||
|
||||
if (canEmbed(url)) {
|
||||
return {
|
||||
url: link,
|
||||
type: 'iframe',
|
||||
patternThumbnailUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (mime?.startsWith('image')) {
|
||||
return { url: link, type: 'image' };
|
||||
}
|
||||
if (mime?.startsWith('video')) {
|
||||
return { url: link, type: 'video' };
|
||||
}
|
||||
if (mime?.startsWith('audio')) {
|
||||
return { url: link, type: 'audio' };
|
||||
}
|
||||
return { url: link, type: 'webpage' };
|
||||
};
|
||||
|
||||
export const getLinkMediaInfoMemoized = memoize(getLinkMediaInfo, { max: 1000 });
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
export const getFormattedDate = (commentTimestamp: number) => {
|
||||
if (commentTimestamp === undefined || isNaN(commentTimestamp)) {
|
||||
return '';
|
||||
}
|
||||
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
const string = new Intl.DateTimeFormat(locale, {
|
||||
hour12: false,
|
||||
year: '2-digit',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
weekday: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
}).format(new Date(commentTimestamp * 1000));
|
||||
if (locale.startsWith('ar')) {
|
||||
return string;
|
||||
}
|
||||
const items = string.split(/,* /);
|
||||
if (items.length === 3) {
|
||||
const itemIsNumber = [items[0][0].match(/[0-9]/), items[1][0].match(/[0-9]/)];
|
||||
if (itemIsNumber[0] && itemIsNumber[1]) {
|
||||
return `${items[0]}(${items[2]})${items[1]}`;
|
||||
}
|
||||
if (itemIsNumber[0] && !itemIsNumber[1]) {
|
||||
return `${items[0]}(${items[1]})${items[2]}`;
|
||||
}
|
||||
if (!itemIsNumber[0] && itemIsNumber[1]) {
|
||||
return `${items[1]}(${items[0]})${items[2]}`;
|
||||
}
|
||||
}
|
||||
return string;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
export const getHostname = (url: string) => {
|
||||
try {
|
||||
return new URL(url).hostname.replace(/^www\./, '');
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const isValidURL = (url: string) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const copyShareLinkToClipboard = (subplebbitAddress: string, cid: string) => {
|
||||
const shareLink = `https://pleb.bz/p/${subplebbitAddress}/c/${cid}`;
|
||||
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(shareLink);
|
||||
} else {
|
||||
alert('Your browser does not support clipboard API');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user