mirror of
https://github.com/ilyakooo0/urbit.git
synced 2025-01-04 21:33:41 +03:00
interface: added oembed parser
This commit is contained in:
parent
6e2ce697f9
commit
8729020fd3
21
pkg/interface/package-lock.json
generated
21
pkg/interface/package-lock.json
generated
@ -6807,6 +6807,11 @@
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
|
||||
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
|
||||
},
|
||||
"node-forge": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz",
|
||||
@ -7058,6 +7063,14 @@
|
||||
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
|
||||
"dev": true
|
||||
},
|
||||
"oembed-parser": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/oembed-parser/-/oembed-parser-1.4.1.tgz",
|
||||
"integrity": "sha512-1KqnfrXF3TiAQhJ9+vv3dEtMhPSVSOT9D9XPqLjEtaQg5liPc3LQ65YjgKHo7Z/YY/kmZ1PDb5gMcOxxCPPdBA==",
|
||||
"requires": {
|
||||
"node-fetch": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"omit-deep": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/omit-deep/-/omit-deep-0.3.0.tgz",
|
||||
@ -7937,6 +7950,14 @@
|
||||
"xtend": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"react-oembed-container": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-oembed-container/-/react-oembed-container-1.0.0.tgz",
|
||||
"integrity": "sha512-YppvCDgxZkn6qgwAIpxRtmMtxaMpau8yQhm8nzmH7yHpDapmHxzakXvQke5qPfmdYyYW4CsKDfVfGoX14NvQkw==",
|
||||
"requires": {
|
||||
"prop-types": "^15.6.0"
|
||||
}
|
||||
},
|
||||
"react-router": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
|
||||
|
@ -20,6 +20,7 @@
|
||||
"moment": "^2.20.1",
|
||||
"mousetrap": "^1.6.5",
|
||||
"mousetrap-global-bind": "^1.1.0",
|
||||
"oembed-parser": "^1.4.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.5.2",
|
||||
"react-codemirror2": "^6.0.1",
|
||||
@ -29,6 +30,7 @@
|
||||
"react-dom": "^16.8.6",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-oembed-container": "^1.0.0",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"react-window": "^1.8.5",
|
||||
"remark-disable-tokenizers": "^1.0.24",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import BaseApi from "./base";
|
||||
import { StoreState } from "../store/type";
|
||||
import { BackgroundConfig } from "../types/local-update";
|
||||
import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from "../types/local-update";
|
||||
|
||||
export default class LocalApi extends BaseApi<StoreState> {
|
||||
getBaseHash() {
|
||||
@ -69,6 +69,16 @@ export default class LocalApi extends BaseApi<StoreState> {
|
||||
});
|
||||
}
|
||||
|
||||
setRemoteContentPolicy(policy: LocalUpdateRemoteContentPolicy) {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
remoteContentPolicy: policy
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dehydrate() {
|
||||
this.store.dehydrate();
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { StoreState } from '~/store/type';
|
||||
import { Cage } from '~/types/cage';
|
||||
import { LocalUpdate, BackgroundConfig } from '~/types/local-update';
|
||||
|
||||
type LocalState = Pick<StoreState, 'sidebarShown' | 'omniboxShown' | 'baseHash' | 'hideAvatars' | 'hideNicknames' | 'background' | 'dark' | 'suspendedFocus'>;
|
||||
type LocalState = Pick<StoreState, 'sidebarShown' | 'omniboxShown' | 'baseHash' | 'hideAvatars' | 'hideNicknames' | 'background' | 'dark' | 'suspendedFocus' | 'remoteContentPolicy'>;
|
||||
|
||||
export default class LocalReducer<S extends LocalState> {
|
||||
rehydrate(state: S) {
|
||||
@ -18,7 +18,7 @@ export default class LocalReducer<S extends LocalState> {
|
||||
}
|
||||
|
||||
dehydrate(state: S) {
|
||||
const json = _.pick(state, ['hideNicknames' , 'hideAvatars' , 'background']);
|
||||
const json = _.pick(state, ['hideNicknames' , 'hideAvatars' , 'background', 'remoteContentPolicy']);
|
||||
localStorage.setItem('localReducer', JSON.stringify(json));
|
||||
}
|
||||
reduce(json: Cage, state: S) {
|
||||
@ -31,6 +31,7 @@ export default class LocalReducer<S extends LocalState> {
|
||||
this.hideAvatars(data, state)
|
||||
this.hideNicknames(data, state)
|
||||
this.omniboxShown(data, state);
|
||||
this.remoteContentPolicy(data, state);
|
||||
}
|
||||
}
|
||||
baseHash(obj: LocalUpdate, state: S) {
|
||||
@ -70,6 +71,12 @@ export default class LocalReducer<S extends LocalState> {
|
||||
}
|
||||
}
|
||||
|
||||
remoteContentPolicy(obj: LocalUpdate, state: S) {
|
||||
if('remoteContentPolicy' in obj) {
|
||||
state.remoteContentPolicy = obj.remoteContentPolicy;
|
||||
}
|
||||
}
|
||||
|
||||
hideAvatars(obj: LocalUpdate, state: S) {
|
||||
if('hideAvatars' in obj) {
|
||||
state.hideAvatars = obj.hideAvatars;
|
||||
|
@ -54,6 +54,12 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
suspendedFocus: null,
|
||||
baseHash: null,
|
||||
background: undefined,
|
||||
remoteContentPolicy: {
|
||||
imageShown: true,
|
||||
audioShown: true,
|
||||
videoShown: true,
|
||||
oembedShown: false,
|
||||
},
|
||||
hideAvatars: false,
|
||||
hideNicknames: false,
|
||||
invites: {},
|
||||
|
@ -11,7 +11,7 @@ import { Permissions } from '~/types/permission-update';
|
||||
import { LaunchState, WeatherState } from '~/types/launch-update';
|
||||
import { LinkComments, LinkCollections, LinkSeen } from '~/types/link-update';
|
||||
import { ConnectionStatus } from '~/types/connection';
|
||||
import { BackgroundConfig } from '~/types/local-update';
|
||||
import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from '~/types/local-update';
|
||||
|
||||
export interface StoreState {
|
||||
// local state
|
||||
@ -22,6 +22,7 @@ export interface StoreState {
|
||||
connection: ConnectionStatus;
|
||||
baseHash: string | null;
|
||||
background: BackgroundConfig;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
// invite state
|
||||
|
@ -1,12 +1,3 @@
|
||||
export type LocalUpdate =
|
||||
LocalUpdateSidebarToggle
|
||||
| LocalUpdateSetDark
|
||||
| LocalUpdateBaseHash
|
||||
| LocalUpdateBackgroundConfig
|
||||
| LocalUpdateHideAvatars
|
||||
| LocalUpdateHideNicknames
|
||||
| LocalUpdateSetOmniboxShown;
|
||||
|
||||
interface LocalUpdateSidebarToggle {
|
||||
sidebarToggle: boolean;
|
||||
}
|
||||
@ -31,7 +22,16 @@ interface LocalUpdateHideNicknames {
|
||||
hideNicknames: boolean;
|
||||
}
|
||||
|
||||
export type BackgroundConfig = BackgroundConfigUrl | BackgroundConfigColor | undefined;
|
||||
interface LocalUpdateSetOmniboxShown {
|
||||
omniboxShown: boolean;
|
||||
}
|
||||
|
||||
export interface LocalUpdateRemoteContentPolicy {
|
||||
imageShown: boolean;
|
||||
audioShown: boolean;
|
||||
videoShown: boolean;
|
||||
oembedShown: boolean;
|
||||
}
|
||||
|
||||
interface BackgroundConfigUrl {
|
||||
type: 'url';
|
||||
@ -43,6 +43,14 @@ interface BackgroundConfigColor {
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface LocalUpdateSetOmniboxShown {
|
||||
omniboxShown: boolean;
|
||||
}
|
||||
export type BackgroundConfig = BackgroundConfigUrl | BackgroundConfigColor | undefined;
|
||||
|
||||
export type LocalUpdate =
|
||||
LocalUpdateSidebarToggle
|
||||
| LocalUpdateSetDark
|
||||
| LocalUpdateBaseHash
|
||||
| LocalUpdateBackgroundConfig
|
||||
| LocalUpdateHideAvatars
|
||||
| LocalUpdateHideNicknames
|
||||
| LocalUpdateSetOmniboxShown
|
||||
| LocalUpdateRemoteContentPolicy;
|
@ -89,7 +89,8 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
|
||||
pendingMessages,
|
||||
groups,
|
||||
hideAvatars,
|
||||
hideNicknames
|
||||
hideNicknames,
|
||||
remoteContentPolicy
|
||||
} = props;
|
||||
|
||||
const renderChannelSidebar = (props, station?) => (
|
||||
@ -271,6 +272,7 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
|
||||
chatInitialized={chatInitialized}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
{...props}
|
||||
/>
|
||||
</Skeleton>
|
||||
|
@ -15,6 +15,7 @@ import { Path, Patp } from "~/types/noun";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { Association } from "~/types/metadata-update";
|
||||
import {Group} from "~/types/group-update";
|
||||
import { LocalUpdateRemoteContentPolicy } from "~/types";
|
||||
|
||||
|
||||
type ChatScreenProps = RouteComponentProps<{
|
||||
@ -38,6 +39,7 @@ type ChatScreenProps = RouteComponentProps<{
|
||||
envelopes: Envelope[];
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
};
|
||||
|
||||
interface ChatScreenState {
|
||||
@ -131,6 +133,7 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
|
||||
api={props.api}
|
||||
hideNicknames={props.hideNicknames}
|
||||
hideAvatars={props.hideAvatars}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
/>
|
||||
<ChatInput
|
||||
api={props.api}
|
||||
|
@ -17,7 +17,8 @@ export const ChatMessage = (props) => {
|
||||
contacts,
|
||||
unreadRef,
|
||||
hideAvatars,
|
||||
hideNicknames
|
||||
hideNicknames,
|
||||
remoteContentPolicy
|
||||
} = props;
|
||||
|
||||
// Render sigil if previous message is not by the same sender
|
||||
@ -46,6 +47,7 @@ export const ChatMessage = (props) => {
|
||||
association={association}
|
||||
hideNicknames={hideNicknames}
|
||||
hideAvatars={hideAvatars}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -187,6 +187,7 @@ export class ChatWindow extends Component {
|
||||
contacts={props.contacts}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
@ -1,106 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Button } from '@tlon/indigo-react';
|
||||
|
||||
const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i);
|
||||
|
||||
const YOUTUBE_REGEX =
|
||||
new RegExp(
|
||||
String(/(?:https?:\/\/(?:[a-z]+.)?)/.source) // protocol
|
||||
+ /(?:youtu\.?be(?:\.com)?\/)(?:embed\/)?/.source // short and long-links
|
||||
+ /(?:(?:(?:(?:watch\?)?(?:time_continue=(?:[0-9]+))?.+v=)?([a-zA-Z0-9_-]+))(?:\?t\=(?:[0-9a-zA-Z]+))?)/.source // id
|
||||
);
|
||||
|
||||
export default class UrlContent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
unfold: false,
|
||||
copied: false
|
||||
};
|
||||
this.unfoldEmbed = this.unfoldEmbed.bind(this);
|
||||
}
|
||||
|
||||
unfoldEmbed(id) {
|
||||
let unfoldState = this.state.unfold;
|
||||
unfoldState = !unfoldState;
|
||||
this.setState({ unfold: unfoldState });
|
||||
this.iframe.setAttribute('src', this.iframe.dataset.src);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const content = props.content;
|
||||
const imgMatch = IMAGE_REGEX.exec(props.content.url);
|
||||
const ytMatch = YOUTUBE_REGEX.exec(props.content.url);
|
||||
|
||||
let contents = content.url;
|
||||
if (imgMatch) {
|
||||
contents = (
|
||||
<img
|
||||
className="o-80-d"
|
||||
src={content.url}
|
||||
style={{
|
||||
maxWidth: '18rem'
|
||||
}}
|
||||
></img>
|
||||
);
|
||||
return (
|
||||
<a className='f7 lh-copy v-top word-break-all'
|
||||
href={content.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{contents}
|
||||
</a>
|
||||
);
|
||||
} else if (ytMatch) {
|
||||
contents = (
|
||||
<div className={'embed-container mb2 w-100 w-75-l w-50-xl ' +
|
||||
((this.state.unfold === true)
|
||||
? 'db' : 'dn')}
|
||||
>
|
||||
<iframe
|
||||
ref={(el) => {
|
||||
this.iframe = el;
|
||||
}}
|
||||
width="560"
|
||||
height="315"
|
||||
data-src={`https://www.youtube.com/embed/${ytMatch[1]}`}
|
||||
frameBorder="0" allow="picture-in-picture, fullscreen"
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<a href={content.url}
|
||||
className='f7 lh-copy v-top bb b--white-d word-break-all'
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{content.url}
|
||||
</a>
|
||||
<Button
|
||||
border={1}
|
||||
style={{ display: 'inline-flex', height: '1.66em' }} // Height is hacked to line-height until Button supports proper size
|
||||
ml={1}
|
||||
onClick={e => this.unfoldEmbed()}
|
||||
>
|
||||
{this.state.unfold ? 'collapse' : 'embed'}
|
||||
</Button>
|
||||
{contents}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a className='f7 lh-copy v-top bb b--white-d b--black word-break-all'
|
||||
href={content.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{contents}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
||||
|
||||
import TextContent from './content/text';
|
||||
import CodeContent from './content/code';
|
||||
import UrlContent from './content/url';
|
||||
import RemoteContent from '~/views/components/RemoteContent';
|
||||
|
||||
|
||||
export default class MessageContent extends Component {
|
||||
@ -15,7 +15,18 @@ export default class MessageContent extends Component {
|
||||
if ('code' in content) {
|
||||
return <CodeContent content={content} />;
|
||||
} else if ('url' in content) {
|
||||
return <UrlContent content={content} />;
|
||||
return (
|
||||
<RemoteContent
|
||||
url={content.url}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
imageProps={{style: {
|
||||
maxWidth: '18rem'
|
||||
}}}
|
||||
videoProps={{style: {
|
||||
maxWidth: '18rem'
|
||||
}}}
|
||||
/>
|
||||
);
|
||||
} else if ('me' in content) {
|
||||
return (
|
||||
<p className='f7 i lh-copy v-top'>
|
||||
|
@ -31,7 +31,7 @@ export const Message = (props) => {
|
||||
<p className="child pt2 pl2 pr1 mono f9 gray2 dib">{timestamp}</p>
|
||||
<div className="fr f7 clamp-message white-d pr3 lh-copy"
|
||||
style={{ flexGrow: 1 }}>
|
||||
<MessageContent letter={props.msg.letter} />
|
||||
<MessageContent letter={props.msg.letter} remoteContentPolicy={props.remoteContentPolicy}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -98,7 +98,7 @@ const renderWithSigil = (props, timestamp) => {
|
||||
{datestamp}
|
||||
</p>
|
||||
</div>
|
||||
<MessageContent letter={props.msg.letter} />
|
||||
<MessageContent letter={props.msg.letter} remoteContentPolicy={props.remoteContentPolicy} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -116,20 +116,8 @@ h2 {
|
||||
100% {transform: rotate(360deg);}
|
||||
}
|
||||
|
||||
/* embeds */
|
||||
.embed-container {
|
||||
position: relative;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
padding-bottom: 28.125%;
|
||||
}
|
||||
|
||||
.embed-container iframe, .embed-container object, .embed-container embed {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.embed-container iframe {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mh-16 {
|
||||
@ -188,9 +176,6 @@ h2 {
|
||||
.h-100-minus-96-s {
|
||||
height: calc(100% - 96px);
|
||||
}
|
||||
.embed-container {
|
||||
padding-bottom: 56.25%;
|
||||
}
|
||||
.unread-notice {
|
||||
top: 96px;
|
||||
}
|
||||
@ -200,18 +185,12 @@ h2 {
|
||||
.flex-basis-250-m {
|
||||
flex-basis: 250px;
|
||||
}
|
||||
.embed-container {
|
||||
padding-bottom: 56.25%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 46.875em) and (max-width: 60em) {
|
||||
.flex-basis-250-l {
|
||||
flex-basis: 250px;
|
||||
}
|
||||
.embed-container {
|
||||
padding-bottom: 37.5%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 60em) {
|
||||
|
@ -59,7 +59,7 @@ export class LinksApp extends Component {
|
||||
props.invites : {};
|
||||
|
||||
const listening = props.linkListening;
|
||||
const { api, sidebarShown, hideAvatars, hideNicknames, s3 } = this.props;
|
||||
const { api, sidebarShown, hideAvatars, hideNicknames, s3, remoteContentPolicy } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -319,6 +319,7 @@ export class LinksApp extends Component {
|
||||
api={api}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
|
@ -2,6 +2,8 @@ import React, { Component } from 'react';
|
||||
import { cite } from '~/logic/lib/util';
|
||||
import moment from 'moment';
|
||||
|
||||
import RemoteContent from "~/views/components/RemoteContent";
|
||||
|
||||
export class LinkPreview extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -27,26 +29,6 @@ export class LinkPreview extends Component {
|
||||
timeSinceLinkPost: this.getTimeSinceLinkPost()
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
// check for soundcloud for fetching embed
|
||||
const soundcloudRegex = new RegExp(String(/(https?:\/\/(?:www.)?soundcloud.com\/[\w-]+\/?(?:sets\/)?[\w-]+)/.source)
|
||||
);
|
||||
|
||||
const isSoundcloud = soundcloudRegex.exec(this.props.url);
|
||||
|
||||
if (isSoundcloud && this.state.embed === '') {
|
||||
fetch(
|
||||
'https://soundcloud.com/oembed?format=json&url=' +
|
||||
encodeURIComponent(this.props.url))
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((json) => {
|
||||
this.setState({ embed: json.html });
|
||||
});
|
||||
} else if (!isSoundcloud) {
|
||||
this.setState({ embed: '' });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -66,6 +48,16 @@ export class LinkPreview extends Component {
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
const embed = (
|
||||
<RemoteContent
|
||||
unfold={true}
|
||||
renderUrl={false}
|
||||
url={props.url}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
className="mw-100"
|
||||
/>
|
||||
);
|
||||
|
||||
const URLparser = new RegExp(
|
||||
/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/
|
||||
);
|
||||
@ -76,42 +68,6 @@ export class LinkPreview extends Component {
|
||||
hostname = hostname[4];
|
||||
}
|
||||
|
||||
const imgMatch = /(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|webm|WEBM)$/.exec(
|
||||
props.url
|
||||
);
|
||||
|
||||
const youTubeRegex = new RegExp(
|
||||
String(/(?:https?:\/\/(?:[a-z]+.)?)/.source) + // protocol
|
||||
/(?:youtu\.?be(?:\.com)?\/)(?:embed\/)?/.source + // short and long-links
|
||||
/(?:(?:(?:(?:watch\?)?(?:time_continue=(?:[0-9]+))?.+v=)?([a-zA-Z0-9_-]+))(?:\?t\=(?:[0-9a-zA-Z]+))?)/.source // id
|
||||
);
|
||||
|
||||
const ytMatch = youTubeRegex.exec(props.url);
|
||||
|
||||
let embed = '';
|
||||
|
||||
if (imgMatch) {
|
||||
embed = <a href={props.url}
|
||||
target="_blank"
|
||||
style={{ width: 'max-content' }}
|
||||
>
|
||||
<img src={props.url} style={{ maxHeight: '500px', maxWidth: '100%' }} />
|
||||
</a>;
|
||||
}
|
||||
|
||||
if (ytMatch) {
|
||||
embed = (
|
||||
<iframe
|
||||
ref="iframe"
|
||||
width="560"
|
||||
height="315"
|
||||
src={`https://www.youtube.com/embed/${ytMatch[1]}`}
|
||||
frameBorder="0"
|
||||
allow="picture-in-picture, fullscreen"
|
||||
></iframe>
|
||||
);
|
||||
}
|
||||
|
||||
const showNickname = props.nickname && !props.hideNicknames;
|
||||
|
||||
const nameClass = showNickname ? 'inter' : 'mono';
|
||||
@ -119,9 +75,9 @@ export class LinkPreview extends Component {
|
||||
return (
|
||||
<div className="pb6 w-100">
|
||||
<div
|
||||
className={'w-100 tc ' + (ytMatch ? 'links embed-container' : '')}
|
||||
className={'w-100 tc'}
|
||||
>
|
||||
{embed || <div dangerouslySetInnerHTML={{ __html: this.state.embed }} />}
|
||||
{embed}
|
||||
</div>
|
||||
<div className="flex flex-column ml2 pt6 flex-auto">
|
||||
<a href={props.url} className="w-100 flex" target="_blank" rel="noopener noreferrer">
|
||||
|
@ -167,6 +167,7 @@ export class LinkDetail extends Component {
|
||||
linkIndex={props.linkIndex}
|
||||
time={this.state.data.time}
|
||||
hideNicknames={props.hideNicknames}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
/>
|
||||
<div className="relative">
|
||||
<div className={'relative ba br1 mt6 mb6 ' + focus}>
|
||||
|
@ -0,0 +1,94 @@
|
||||
import React from "react";
|
||||
import { Box, Button, Checkbox } from '@tlon/indigo-react';
|
||||
import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { LocalUpdateRemoteContentPolicy } from "~/types/local-update";
|
||||
|
||||
const formSchema = Yup.object().shape({
|
||||
imageShown: Yup.boolean(),
|
||||
audioShown: Yup.boolean(),
|
||||
videoShown: Yup.boolean(),
|
||||
oembedShown: Yup.boolean()
|
||||
});
|
||||
|
||||
interface FormSchema {
|
||||
imageShown: boolean;
|
||||
audioShown: boolean;
|
||||
videoShown: boolean;
|
||||
oembedShown: boolean;
|
||||
}
|
||||
|
||||
interface RemoteContentFormProps {
|
||||
api: GlobalApi;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
}
|
||||
|
||||
export default function RemoteContentForm(props: RemoteContentFormProps) {
|
||||
const { api, remoteContentPolicy } = props;
|
||||
const imageShown = remoteContentPolicy.imageShown;
|
||||
const audioShown = remoteContentPolicy.audioShown;
|
||||
const videoShown = remoteContentPolicy.videoShown;
|
||||
const oembedShown = remoteContentPolicy.oembedShown;
|
||||
return (
|
||||
<Formik
|
||||
validationSchema={formSchema}
|
||||
initialValues={
|
||||
{
|
||||
imageShown,
|
||||
audioShown,
|
||||
videoShown,
|
||||
oembedShown
|
||||
} as FormSchema
|
||||
}
|
||||
onSubmit={(values, actions) => {
|
||||
api.local.setRemoteContentPolicy({
|
||||
imageShown: values.imageShown,
|
||||
audioShown: values.audioShown,
|
||||
videoShown: values.videoShown,
|
||||
oembedShown: values.oembedShown
|
||||
});
|
||||
api.local.dehydrate();
|
||||
actions.setSubmitting(false);
|
||||
}}
|
||||
>
|
||||
{(props) => (
|
||||
<Form>
|
||||
<Box
|
||||
display="grid"
|
||||
gridTemplateColumns="1fr"
|
||||
gridTemplateRows="audio"
|
||||
gridRowGap={3}
|
||||
>
|
||||
<Box color="black" fontSize={1} mb={3} fontWeight={900}>
|
||||
Remote Content
|
||||
</Box>
|
||||
<Box>
|
||||
<Checkbox
|
||||
label="Load images"
|
||||
id="imageShown"
|
||||
/>
|
||||
<Checkbox
|
||||
label="Load audio files"
|
||||
id="audioShown"
|
||||
/>
|
||||
<Checkbox
|
||||
label="Load video files"
|
||||
id="videoShown"
|
||||
/>
|
||||
<Checkbox
|
||||
label="Load embedded content"
|
||||
id="oembedShown"
|
||||
caption="Embedded content may contain scripts"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Button border={1} borderColor="washedGray" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
@ -19,6 +19,7 @@ import { StoreState } from "../../../store/type";
|
||||
import DisplayForm from "./lib/DisplayForm";
|
||||
import S3Form from "./lib/S3Form";
|
||||
import SecuritySettings from "./lib/Security";
|
||||
import RemoteContentForm from "./lib/RemoteContent";
|
||||
|
||||
type ProfileProps = StoreState & { api: GlobalApi; ship: string };
|
||||
|
||||
@ -30,6 +31,7 @@ export default function Settings({
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
background,
|
||||
remoteContentPolicy
|
||||
}: ProfileProps) {
|
||||
return (
|
||||
<Box
|
||||
@ -51,6 +53,7 @@ export default function Settings({
|
||||
background={background}
|
||||
s3={s3}
|
||||
/>
|
||||
<RemoteContentForm {...{api, remoteContentPolicy}} />
|
||||
<S3Form api={api} s3={s3} />
|
||||
<SecuritySettings api={api} />
|
||||
</Box>
|
||||
|
148
pkg/interface/src/views/components/RemoteContent.tsx
Normal file
148
pkg/interface/src/views/components/RemoteContent.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { LocalUpdateRemoteContentPolicy } from "~/types/local-update";
|
||||
import { Button } from '@tlon/indigo-react';
|
||||
import { hasProvider } from 'oembed-parser';
|
||||
import EmbedContainer from 'react-oembed-container';
|
||||
|
||||
interface RemoteContentProps {
|
||||
url: string;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
unfold: boolean;
|
||||
renderUrl: boolean;
|
||||
imageProps: any;
|
||||
audioProps: any;
|
||||
videoProps: any;
|
||||
oembedProps: any;
|
||||
}
|
||||
|
||||
interface RemoteContentState {
|
||||
unfold: boolean;
|
||||
embed: any | undefined;
|
||||
}
|
||||
|
||||
const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i);
|
||||
const AUDIO_REGEX = new RegExp(/(mp3|wav|ogg)$/i);
|
||||
const VIDEO_REGEX = new RegExp(/(mov|mp4|ogv)$/i);
|
||||
|
||||
export default class RemoteContent extends Component<RemoteContentProps, RemoteContentState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
unfold: props.unfold || false,
|
||||
embed: undefined
|
||||
};
|
||||
this.unfoldEmbed = this.unfoldEmbed.bind(this);
|
||||
this.loadOembed = this.loadOembed.bind(this);
|
||||
this.wrapInLink = this.wrapInLink.bind(this);
|
||||
}
|
||||
|
||||
unfoldEmbed() {
|
||||
let unfoldState = this.state.unfold;
|
||||
unfoldState = !unfoldState;
|
||||
this.setState({ unfold: unfoldState });
|
||||
}
|
||||
|
||||
loadOembed() {
|
||||
fetch(`https://noembed.com/embed?url=${this.props.url}`)
|
||||
.then(response => response.json())
|
||||
.then((result) => {
|
||||
this.setState({ embed: result });
|
||||
}).catch((error) => {
|
||||
this.setState({ embed: 'error' });
|
||||
console.log('error fetching oembed', error);
|
||||
});
|
||||
}
|
||||
|
||||
wrapInLink(contents) {
|
||||
return (<a
|
||||
href={this.props.url}
|
||||
className={`f7 lh-copy v-top word-break-all ${(typeof contents === 'string') ? 'bb b--white-d b--black' : ''}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{contents}
|
||||
</a>);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
remoteContentPolicy,
|
||||
url,
|
||||
unfold = false,
|
||||
renderUrl = true,
|
||||
imageProps = {},
|
||||
audioProps = {},
|
||||
videoProps = {},
|
||||
oembedProps = {},
|
||||
...props
|
||||
} = this.props;
|
||||
const isImage = IMAGE_REGEX.test(url);
|
||||
const isAudio = AUDIO_REGEX.test(url);
|
||||
const isVideo = VIDEO_REGEX.test(url);
|
||||
const isOembed = hasProvider(url);
|
||||
|
||||
if (isImage && remoteContentPolicy.imageShown) {
|
||||
return this.wrapInLink(
|
||||
<img
|
||||
src={url}
|
||||
{...imageProps}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else if (isAudio && remoteContentPolicy.audioShown) {
|
||||
return (
|
||||
<>
|
||||
{renderUrl ? this.wrapInLink(url) : null}
|
||||
<audio
|
||||
controls
|
||||
className="db"
|
||||
src={url}
|
||||
{...audioProps}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else if (isVideo && remoteContentPolicy.videoShown) {
|
||||
return (
|
||||
<>
|
||||
{renderUrl ? this.wrapInLink(url) : null}
|
||||
<video
|
||||
controls
|
||||
className="db"
|
||||
src={url}
|
||||
{...videoProps}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else if (isOembed && remoteContentPolicy.oembedShown) {
|
||||
this.loadOembed();
|
||||
return (
|
||||
<Fragment>
|
||||
{renderUrl ? this.wrapInLink(this.state.embed && this.state.embed.title ? this.state.embed.title : url) : null}
|
||||
{this.state.embed !== 'error' && !unfold ? <Button
|
||||
border={1}
|
||||
style={{ display: 'inline-flex', height: '1.66em' }} // Height is hacked to line-height until Button supports proper size
|
||||
ml={1}
|
||||
onClick={this.unfoldEmbed}
|
||||
>
|
||||
{this.state.unfold ? 'collapse' : 'expand'}
|
||||
</Button> : null}
|
||||
<div
|
||||
className={'embed-container mb2 w-100 w-75-l w-50-xl ' + (this.state.unfold ? 'db' : 'dn')}
|
||||
{...oembedProps}
|
||||
{...props}
|
||||
>
|
||||
{this.state.embed && this.state.embed.html && this.state.unfold
|
||||
? <EmbedContainer markup={this.state.embed.html}>
|
||||
<div dangerouslySetInnerHTML={{__html: this.state.embed.html}}></div>
|
||||
</EmbedContainer>
|
||||
: null}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
return renderUrl ? this.wrapInLink(url) : null;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user