Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5c81ba9
Add room list avatar option & youtube embed player option
BatteredBunny Oct 8, 2022
522ca5b
Ran eslint
BatteredBunny Oct 8, 2022
7733bae
Made compontent more generic and replaced link finder function
BatteredBunny Oct 10, 2022
297c794
Added Youtube shorts support
BatteredBunny Oct 10, 2022
70a1ec6
Will no longer display links in sticker and file names
BatteredBunny Oct 14, 2022
24acf16
Merge branch 'dev' into dev
BatteredBunny Oct 16, 2022
01999dd
Merge branch 'cinnyapp:dev' into url-preview
BatteredBunny Nov 16, 2022
e0eae96
Added basic url previews
BatteredBunny Nov 17, 2022
832a340
Added setting to turn off url previews
BatteredBunny Nov 17, 2022
13c4e14
Url preview error handling improved, Tries to parse image height and …
BatteredBunny Nov 17, 2022
618602d
Added try catch to getUrlPreview
BatteredBunny Nov 17, 2022
d36a6f6
Organized settings, made Embed compontent more logical
BatteredBunny Nov 17, 2022
f50e37e
Merge pull request #1 from ayes-web/url-preview
BatteredBunny Nov 17, 2022
dd22a92
Adjusted styling and classes
BatteredBunny Nov 18, 2022
079c5d7
Improved room list avatar code
BatteredBunny Nov 18, 2022
f474e9d
Merge branch 'dev' into dev
BatteredBunny Jan 10, 2023
681a097
Merge branch 'dev' into dev
BatteredBunny Jan 18, 2023
8588d40
Merge branch 'dev' into dev
BatteredBunny Feb 17, 2023
3177fa4
Merge branch 'cinnyapp:dev' into dev
BatteredBunny Jun 6, 2023
1d658a3
Merge branch 'dev' into dev
BatteredBunny Jun 17, 2023
57a1693
Merge branch 'dev' into dev
BatteredBunny Aug 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 199 additions & 1 deletion src/app/molecules/media/Media.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import ExternalSVG from '../../../../public/res/ic/outlined/external.svg';
import PlaySVG from '../../../../public/res/ic/outlined/play.svg';

import { getBlobSafeMimeType } from '../../../util/mimetypes';
import initMatrix from '../../../client/initMatrix';
import settings from '../../../client/state/settings';

async function getDecryptedBlob(response, type, decryptData) {
const arrayBuffer = await response.arrayBuffer();
Expand Down Expand Up @@ -361,6 +363,202 @@ Video.propTypes = {
blurhash: PropTypes.string,
};

function IframePlayer({
children, link, sitename, title, thumbnail,
}) {
const [videoStarted, setVideoStarted] = useState(false);

const handlePlayVideo = () => {
setVideoStarted(true);
};

return (
<div className="iframeplayer">
<div className="file-container">
<div className="file-header">
<Text className="file-name" variant="b3">{`${sitename} - ${title}`}</Text>

<IconButton
size="extra-small"
tooltip="Open in new tab"
src={ExternalSVG}
onClick={() => window.open(link)}
/>
</div>

<div className="video-container">
{videoStarted ? (
<div>
{children}
</div>
) : (
<>
<img src={thumbnail} alt={`${sitename} thumbnail`} />
<IconButton onClick={handlePlayVideo} tooltip="Play video" src={PlaySVG} />
</>
)}
</div>
</div>
</div>
);
}
IframePlayer.propTypes = {
children: PropTypes.node.isRequired,
link: PropTypes.string.isRequired,
sitename: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
thumbnail: PropTypes.string.isRequired,
};

function Embed({ link }) {
const url = new URL(link);

if (settings.showYoutubeEmbedPlayer && (((url.host === 'www.youtube.com' || url.host === 'youtube.com') && (url.pathname === '/watch' || url.pathname.startsWith('/shorts/'))) || url.host === 'youtu.be' || url.host === 'www.youtu.be')) {
return <YoutubeEmbed link={link} />;
}

const [urlPreviewInfo, setUrlPreviewInfo] = useState();
const mx = initMatrix.matrixClient;

useEffect(() => {
let unmounted = false;

async function getUrlPreview() {
try {
const info = await mx.getUrlPreview(link, 0);
if (unmounted) return;
setUrlPreviewInfo(info);
} catch {
setUrlPreviewInfo();
}
}

getUrlPreview();

return () => {
unmounted = true;
};
});

if (urlPreviewInfo != null) {
const imageURL = urlPreviewInfo['og:image'] || urlPreviewInfo['og:image:secure_url'];
const image = (imageURL != null) ? (
<Image
link={mx.mxcUrlToHttp(imageURL)}
height={urlPreviewInfo['og:image:height'] != null ? parseInt(urlPreviewInfo['og:image:height'], 10) : null}
width={urlPreviewInfo['og:image:width'] != null ? parseInt(urlPreviewInfo['og:image:width'], 10) : null}
name={urlPreviewInfo['og:image:alt'] || urlPreviewInfo['og:site_name'] || ''}
type={urlPreviewInfo['og:image:type'] != null ? urlPreviewInfo['og:image:type'] : null}
/>
) : null;

// Image only embed
if (image != null && urlPreviewInfo['og:title'] == null && urlPreviewInfo['og:description'] == null) {
return (
<div className="embed-container">
<div className="file-container">
{image}
</div>
</div>
);
}

const embedTitle = urlPreviewInfo['og:title'] || urlPreviewInfo['og:site_name'];

return (
<div className="embed-container">
<div className="file-container embed">
<div className="embed-text">
{(embedTitle != null) && (
<Text className="embed-title" variant="h2">
{embedTitle}
</Text>
)}

{(urlPreviewInfo['og:description'] != null) && (
<Text className="embed-description" variant="b3">
{urlPreviewInfo['og:description']}
</Text>
)}
</div>

<div className="embed-media">
{image}
</div>
</div>
</div>
);
}

return null;
}
Embed.propTypes = {
link: PropTypes.string.isRequired,
};

function YoutubeEmbed({ link }) {
const [urlPreviewInfo, setUrlPreviewInfo] = useState(null);
const mx = initMatrix.matrixClient;
const url = new URL(link);

// fix for no embed information on www.youtu.be
if (url.host === 'www.youtu.be') {
url.host = 'youtu.be';
}

useEffect(() => {
let unmounted = false;

async function getThumbnail() {
const info = await mx.getUrlPreview(url.toString(), 0);
if (unmounted) return;

setUrlPreviewInfo(info);
}

getThumbnail();

return () => {
unmounted = true;
};
});

let videoID;
if (url.host === 'youtu.be' || url.host === 'www.youtu.be') {
videoID = url.pathname.slice(1);
} else if (url.pathname.startsWith('/shorts/')) {
videoID = url.pathname.slice(8);
} else {
videoID = url.searchParams.get('v');
}

let embedURL = `https://www.youtube-nocookie.com/embed/${videoID}?autoplay=1`;
if (url.searchParams.has('t')) { // timestamp flag
embedURL += `&start=${url.searchParams.get('t')}`;
}

if (urlPreviewInfo !== null) {
return (
<div className="embed-container">
<IframePlayer link={link} sitename="Youtube" title={urlPreviewInfo['og:title']} thumbnail={mx.mxcUrlToHttp(urlPreviewInfo['og:image'])}>
<iframe
src={embedURL}
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</IframePlayer>
</div>
);
}

return null;
}
YoutubeEmbed.propTypes = {
link: PropTypes.string.isRequired,
};

export {
File, Image, Sticker, Audio, Video,
File, Image, Sticker, Audio, Video, YoutubeEmbed, Embed, IframePlayer,
};
15 changes: 15 additions & 0 deletions src/app/molecules/media/Media.scss
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,18 @@
width: 100%;
}
}

.embed-container {
margin-top: 5px;

.embed {
color: var(--tc-surface-low);
background-color: var(--bg-surface-hover);
border-radius: calc(var(--bo-radius) / 2);
padding: 15px;

.embed-text {
margin-bottom: 5px;
}
}
}
11 changes: 11 additions & 0 deletions src/app/molecules/message/Message.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React, {
import PropTypes from 'prop-types';
import './Message.scss';

import { find } from 'linkifyjs';
import { twemojify } from '../../../util/twemojify';

import initMatrix from '../../../client/initMatrix';
Expand Down Expand Up @@ -41,6 +42,8 @@ import BinIC from '../../../../public/res/ic/outlined/bin.svg';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
import { getBlobSafeMimeType } from '../../../util/mimetypes';
import { html, plain } from '../../../util/markdown';
import { Embed } from '../media/Media';
import settings from '../../../client/state/settings';

function PlaceholderMessage() {
return (
Expand Down Expand Up @@ -716,6 +719,11 @@ function getEditedBody(editedMEvent) {
return [parsedContent.body, isCustomHTML, newContent.formatted_body ?? null];
}

function findLinks(body) {
return find(body, 'url')
.filter((v, i, a) => a.findIndex((v2) => (v2.href === v.href)) === i);
}

function Message({
mEvent, isBodyOnly, roomTimeline,
focus, fullTime, isEdit, setEdit, cancelEdit,
Expand Down Expand Up @@ -801,6 +809,9 @@ function Message({
isEdited={isEdited}
/>
)}
{settings.showUrlPreview && msgType === 'm.text' && findLinks(body).map((link) => (
<Embed key={link.href} link={link.href} />
))}
{isEdit && (
<MessageEdit
body={(customHTML
Expand Down
5 changes: 3 additions & 2 deletions src/app/organisms/navigation/Selector.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import SpaceOptions from '../../molecules/space-options/SpaceOptions';
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';

import { useForceUpdate } from '../../hooks/useForceUpdate';
import settings from '../../../client/state/settings';

function Selector({
roomId, isDM, drawerPostie, onClick,
Expand Down Expand Up @@ -57,8 +58,8 @@ function Selector({
key={roomId}
name={room.name}
roomId={roomId}
imageSrc={isDM ? imageSrc : null}
iconSrc={isDM ? null : joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())}
imageSrc={isDM || settings.showRoomListAvatar ? imageSrc : null}
iconSrc={isDM || settings.showRoomListAvatar ? null : joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())}
isSelected={navigation.selectedRoomId === roomId}
isMuted={isMuted}
isUnread={!isMuted && noti.hasNoti(roomId)}
Expand Down
37 changes: 36 additions & 1 deletion src/app/organisms/settings/Settings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import settings from '../../../client/state/settings';
import navigation from '../../../client/state/navigation';
import {
toggleSystemTheme, toggleMarkdown, toggleMembershipEvents, toggleNickAvatarEvents,
toggleNotifications, toggleNotificationSounds,
toggleNotifications, toggleNotificationSounds, toggleShowRoomListAvatar,
toggleShowYoutubeEmbedPlayer, toggleShowUrlPreview,
} from '../../../client/action/settings';
import { usePermission } from '../../hooks/usePermission';

Expand Down Expand Up @@ -80,6 +81,40 @@ function AppearanceSection() {
/>
)}
/>
<SettingTile
title="Show room-list avatar"
options={(
<Toggle
isActive={settings.showRoomListAvatar}
onToggle={() => { toggleShowRoomListAvatar(); updateState({}); }}
/>
)}
content={<Text variant="b3">Will show room avatars in the room list.</Text>}
/>
</div>
<div className="settings-appearance__card">
<MenuHeader>URL Previews</MenuHeader>
<SettingTile
title="Show URL previews"
options={(
<Toggle
isActive={settings.showUrlPreview}
onToggle={() => { toggleShowUrlPreview(); updateState({}); }}
/>
)}
content={<Text variant="b3">Show additional info about urls.</Text>}
/>
<SettingTile
title="Show Youtube embed player"
options={(
<Toggle
isActive={settings.showYoutubeEmbedPlayer}
onToggle={() => { toggleShowYoutubeEmbedPlayer(); updateState({}); }}
disabled={!settings.showUrlPreview}
/>
)}
content={<Text variant="b3">Will show a youtube embed player for youtube links.</Text>}
/>
</div>
<div className="settings-appearance__card">
<MenuHeader>Room messages</MenuHeader>
Expand Down
18 changes: 18 additions & 0 deletions src/client/action/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,21 @@ export function toggleNotificationSounds() {
type: cons.actions.settings.TOGGLE_NOTIFICATION_SOUNDS,
});
}

export function toggleShowRoomListAvatar() {
appDispatcher.dispatch({
type: cons.actions.settings.TOGGLE_SHOW_ROOM_LIST_AVATAR,
});
}

export function toggleShowYoutubeEmbedPlayer() {
appDispatcher.dispatch({
type: cons.actions.settings.TOGGLE_SHOW_YOUTUBE_EMBED_PLAYER,
});
}

export function toggleShowUrlPreview() {
appDispatcher.dispatch({
type: cons.actions.settings.TOGGLE_SHOW_URL_PREVIEW,
});
}
6 changes: 6 additions & 0 deletions src/client/state/cons.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ const cons = {
TOGGLE_NICKAVATAR_EVENT: 'TOGGLE_NICKAVATAR_EVENT',
TOGGLE_NOTIFICATIONS: 'TOGGLE_NOTIFICATIONS',
TOGGLE_NOTIFICATION_SOUNDS: 'TOGGLE_NOTIFICATION_SOUNDS',
TOGGLE_SHOW_ROOM_LIST_AVATAR: 'TOGGLE_SHOW_ROOM_LIST_AVATAR',
TOGGLE_SHOW_YOUTUBE_EMBED_PLAYER: 'TOGGLE_SHOW_YOUTUBE_EMBED_PLAYER',
TOGGLE_SHOW_URL_PREVIEW: 'TOGGLE_SHOW_URL_PREVIEW',
},
},
events: {
Expand Down Expand Up @@ -154,6 +157,9 @@ const cons = {
NICKAVATAR_EVENTS_TOGGLED: 'NICKAVATAR_EVENTS_TOGGLED',
NOTIFICATIONS_TOGGLED: 'NOTIFICATIONS_TOGGLED',
NOTIFICATION_SOUNDS_TOGGLED: 'NOTIFICATION_SOUNDS_TOGGLED',
SHOW_ROOM_LIST_AVATAR_TOGGLED: 'SHOW_ROOM_LIST_AVATAR_TOGGLED',
SHOW_YOUTUBE_EMBED_PLAYER_TOGGLED: 'SHOW_YOUTUBE_EMBED_PLAYER_TOGGLED',
TOGGLE_SHOW_URL_PREVIEW: 'TOGGLE_SHOW_URL_PREVIEW',
},
},
};
Expand Down
Loading