First pass at adding oembeds / iframely.
This commit is contained in:
parent
b6ce26023e
commit
55f91ac5dc
11 changed files with 482 additions and 4 deletions
1
ansible/lemmy.yml
vendored
1
ansible/lemmy.yml
vendored
|
@ -35,6 +35,7 @@
|
||||||
with_items:
|
with_items:
|
||||||
- { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' }
|
- { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' }
|
||||||
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf', mode: '0644' }
|
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf', mode: '0644' }
|
||||||
|
- { src: '../docker/iframely.config.local.js', dest: '/lemmy/iframely.config.local.js', mode: '0600' }
|
||||||
|
|
||||||
- name: add config file (only during initial setup)
|
- name: add config file (only during initial setup)
|
||||||
template: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000'
|
template: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000'
|
||||||
|
|
1
ansible/lemmy_dev.yml
vendored
1
ansible/lemmy_dev.yml
vendored
|
@ -37,6 +37,7 @@
|
||||||
with_items:
|
with_items:
|
||||||
- { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' }
|
- { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' }
|
||||||
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf', mode: '0644' }
|
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf', mode: '0644' }
|
||||||
|
- { src: '../docker/iframely.config.local.js', dest: '/lemmy/iframely.config.local.js', mode: '0600' }
|
||||||
|
|
||||||
- name: add config file (only during initial setup)
|
- name: add config file (only during initial setup)
|
||||||
template: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000'
|
template: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000'
|
||||||
|
|
9
ansible/templates/docker-compose.yml
vendored
9
ansible/templates/docker-compose.yml
vendored
|
@ -30,6 +30,14 @@ services:
|
||||||
- lemmy_pictshare:/usr/share/nginx/html/data
|
- lemmy_pictshare:/usr/share/nginx/html/data
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
lemmy_iframely:
|
||||||
|
image: dogbin/iframely:latest
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8061:8061"
|
||||||
|
volumes:
|
||||||
|
- ./iframely.config.local.js:/iframely/config.local.js:ro
|
||||||
|
restart: always
|
||||||
|
|
||||||
postfix:
|
postfix:
|
||||||
image: mwader/postfix-relay
|
image: mwader/postfix-relay
|
||||||
environment:
|
environment:
|
||||||
|
@ -38,3 +46,4 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
lemmy_db:
|
lemmy_db:
|
||||||
lemmy_pictshare:
|
lemmy_pictshare:
|
||||||
|
lemmy_iframely:
|
||||||
|
|
7
ansible/templates/nginx.conf
vendored
7
ansible/templates/nginx.conf
vendored
|
@ -80,6 +80,13 @@ server {
|
||||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /iframely/ {
|
||||||
|
proxy_pass http://0.0.0.0:8061/;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Anonymize IP addresses
|
# Anonymize IP addresses
|
||||||
|
|
8
docker/dev/docker-compose.yml
vendored
8
docker/dev/docker-compose.yml
vendored
|
@ -28,6 +28,14 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- lemmy_pictshare:/usr/share/nginx/html/data
|
- lemmy_pictshare:/usr/share/nginx/html/data
|
||||||
restart: always
|
restart: always
|
||||||
|
lemmy_iframely:
|
||||||
|
image: dogbin/iframely:latest
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8061:8061"
|
||||||
|
volumes:
|
||||||
|
- ../iframely.config.local.js:/iframely/config.local.js:ro
|
||||||
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
lemmy_db:
|
lemmy_db:
|
||||||
lemmy_pictshare:
|
lemmy_pictshare:
|
||||||
|
lemmy_iframely:
|
||||||
|
|
283
docker/iframely.config.local.js
vendored
Normal file
283
docker/iframely.config.local.js
vendored
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
(function() {
|
||||||
|
var config = {
|
||||||
|
|
||||||
|
// Specify a path for custom plugins. Custom plugins will override core plugins.
|
||||||
|
// CUSTOM_PLUGINS_PATH: __dirname + '/yourcustom-plugin-folder',
|
||||||
|
|
||||||
|
DEBUG: false,
|
||||||
|
RICH_LOG_ENABLED: false,
|
||||||
|
|
||||||
|
// For embeds that require render, baseAppUrl will be used as the host.
|
||||||
|
baseAppUrl: "http://yourdomain.com",
|
||||||
|
relativeStaticUrl: "/r",
|
||||||
|
|
||||||
|
// Or just skip built-in renders altogether
|
||||||
|
SKIP_IFRAMELY_RENDERS: true,
|
||||||
|
|
||||||
|
// For legacy reasons the response format of Iframely open-source is
|
||||||
|
// different by default as it does not group the links array by rel.
|
||||||
|
// In order to get the same grouped response as in Cloud API,
|
||||||
|
// add `&group=true` to your request to change response per request
|
||||||
|
// or set `GROUP_LINKS` in your config to `true` for a global change.
|
||||||
|
GROUP_LINKS: true,
|
||||||
|
|
||||||
|
// Number of maximum redirects to follow before aborting the page
|
||||||
|
// request with `redirect loop` error.
|
||||||
|
MAX_REDIRECTS: 4,
|
||||||
|
|
||||||
|
SKIP_OEMBED_RE_LIST: [
|
||||||
|
// /^https?:\/\/yourdomain\.com\//,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Used to pass parameters to the generate functions when creating HTML elements
|
||||||
|
// disableSizeWrapper: Don't wrap element (iframe, video, etc) in a positioned div
|
||||||
|
GENERATE_LINK_PARAMS: {
|
||||||
|
disableSizeWrapper: true
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
|
||||||
|
port: 8061, //can be overridden by PORT env var
|
||||||
|
host: '0.0.0.0', // Dockers beware. See https://github.com/itteco/iframely/issues/132#issuecomment-242991246
|
||||||
|
//can be overridden by HOST env var
|
||||||
|
|
||||||
|
// Optional SSL cert, if you serve under HTTPS.
|
||||||
|
/*
|
||||||
|
ssl: {
|
||||||
|
key: require('fs').readFileSync(__dirname + '/key.pem'),
|
||||||
|
cert: require('fs').readFileSync(__dirname + '/cert.pem'),
|
||||||
|
port: 443
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Supported cache engines:
|
||||||
|
- no-cache - no caching will be used.
|
||||||
|
- node-cache - good for debug, node memory will be used (https://github.com/tcs-de/nodecache).
|
||||||
|
- redis - https://github.com/mranney/node_redis.
|
||||||
|
- memcached - https://github.com/3rd-Eden/node-memcached
|
||||||
|
*/
|
||||||
|
CACHE_ENGINE: 'node-cache',
|
||||||
|
CACHE_TTL: 0, // In seconds.
|
||||||
|
// 0 = 'never expire' for memcached & node-cache to let cache engine decide itself when to evict the record
|
||||||
|
// 0 = 'no cache' for redis. Use high enough (e.g. 365*24*60*60*1000) ttl for similar 'never expire' approach instead
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Redis cache options.
|
||||||
|
REDIS_OPTIONS: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 6379
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Memcached options. See https://github.com/3rd-Eden/node-memcached#server-locations
|
||||||
|
MEMCACHED_OPTIONS: {
|
||||||
|
locations: "127.0.0.1:11211"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Access-Control-Allow-Origin list.
|
||||||
|
allowedOrigins: [
|
||||||
|
"*",
|
||||||
|
"http://another_domain.com"
|
||||||
|
],
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Uncomment to enable plugin testing framework.
|
||||||
|
tests: {
|
||||||
|
mongodb: 'mongodb://localhost:27017/iframely-tests',
|
||||||
|
single_test_timeout: 10 * 1000,
|
||||||
|
plugin_test_period: 2 * 60 * 60 * 1000,
|
||||||
|
relaunch_script_period: 5 * 60 * 1000
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
|
||||||
|
// If there's no response from remote server, the timeout will occur after
|
||||||
|
RESPONSE_TIMEOUT: 5 * 1000, //ms
|
||||||
|
|
||||||
|
/* From v1.4.0, Iframely supports HTTP/2 by default. Disable it, if you'd rather not.
|
||||||
|
Alternatively, you can also disable per origin. See `proxy` option below.
|
||||||
|
*/
|
||||||
|
// DISABLE_HTTP2: true,
|
||||||
|
|
||||||
|
// Customize API calls to oembed endpoints.
|
||||||
|
ADD_OEMBED_PARAMS: [{
|
||||||
|
// Endpoint url regexp array.
|
||||||
|
re: [/^http:\/\/api\.instagram\.com\/oembed/],
|
||||||
|
// Custom get params object.
|
||||||
|
params: {
|
||||||
|
hidecaption: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
re: [/^https:\/\/www\.facebook\.com\/plugins\/page\/oembed\.json/i],
|
||||||
|
params: {
|
||||||
|
show_posts: 0,
|
||||||
|
show_facepile: 0,
|
||||||
|
maxwidth: 600
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
// match i=user or i=moment or i=timeline to configure these types invidually
|
||||||
|
// see params spec at https://dev.twitter.com/web/embedded-timelines/oembed
|
||||||
|
re: [/^https?:\/\/publish\.twitter\.com\/oembed\?i=user/i],
|
||||||
|
params: {
|
||||||
|
limit: 1,
|
||||||
|
maxwidth: 600
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
}, {
|
||||||
|
// Facebook https://developers.facebook.com/docs/plugins/oembed-endpoints
|
||||||
|
re: [/^https:\/\/www\.facebook\.com\/plugins\/\w+\/oembed\.json/i],
|
||||||
|
params: {
|
||||||
|
// Skip script tag and fb-root div.
|
||||||
|
omitscript: true
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}],
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Configure use of HTTP proxies as needed.
|
||||||
|
// You don't have to specify all options per regex - just what you need to override
|
||||||
|
PROXY: [{
|
||||||
|
re: [/^https?:\/\/www\.domain\.com/],
|
||||||
|
proxy_server: 'http://1.2.3.4:8080',
|
||||||
|
user_agent: 'CHANGE YOUR AGENT',
|
||||||
|
headers: {
|
||||||
|
// HTTP headers
|
||||||
|
// Overrides previous params if overlapped.
|
||||||
|
},
|
||||||
|
request_options: {
|
||||||
|
// Refer to: https://github.com/request/request
|
||||||
|
// Overrides previous params if overlapped.
|
||||||
|
},
|
||||||
|
disable_http2: true
|
||||||
|
}],
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Customize API calls to 3rd parties. At the very least - configure required keys.
|
||||||
|
providerOptions: {
|
||||||
|
locale: "en_US", // ISO 639-1 two-letter language code, e.g. en_CA or fr_CH.
|
||||||
|
// Will be added as highest priotity in accept-language header with each request.
|
||||||
|
// Plus is used in FB, YouTube and perhaps other plugins
|
||||||
|
"twitter": {
|
||||||
|
"max-width": 550,
|
||||||
|
"min-width": 250,
|
||||||
|
hide_media: false,
|
||||||
|
hide_thread: false,
|
||||||
|
omit_script: false,
|
||||||
|
center: false,
|
||||||
|
// dnt: true,
|
||||||
|
cache_ttl: 100 * 365 * 24 * 3600 // 100 Years.
|
||||||
|
},
|
||||||
|
readability: {
|
||||||
|
enabled: false
|
||||||
|
// allowPTagDescription: true // to enable description fallback to first paragraph
|
||||||
|
},
|
||||||
|
images: {
|
||||||
|
loadSize: false, // if true, will try an load first bytes of all images to get/confirm the sizes
|
||||||
|
checkFavicon: false // if true, will verify all favicons
|
||||||
|
},
|
||||||
|
tumblr: {
|
||||||
|
consumer_key: "INSERT YOUR VALUE"
|
||||||
|
// media_only: true // disables status embeds for images and videos - will return plain media
|
||||||
|
},
|
||||||
|
google: {
|
||||||
|
// https://developers.google.com/maps/documentation/embed/guide#api_key
|
||||||
|
maps_key: "INSERT YOUR VALUE"
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Optional Camo Proxy to wrap all images: https://github.com/atmos/camo
|
||||||
|
camoProxy: {
|
||||||
|
camo_proxy_key: "INSERT YOUR VALUE",
|
||||||
|
camo_proxy_host: "INSERT YOUR VALUE"
|
||||||
|
// ssl_only: true // will only proxy non-ssl images
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
|
||||||
|
// List of query parameters to add to YouTube and Vimeo frames
|
||||||
|
// Start it with leading "?". Or omit alltogether for default values
|
||||||
|
// API key is optional, youtube will work without it too.
|
||||||
|
// It is probably the same API key you use for Google Maps.
|
||||||
|
youtube: {
|
||||||
|
// api_key: "INSERT YOUR VALUE",
|
||||||
|
get_params: "?rel=0&showinfo=1" // https://developers.google.com/youtube/player_parameters
|
||||||
|
},
|
||||||
|
vimeo: {
|
||||||
|
get_params: "?byline=0&badge=0" // https://developer.vimeo.com/player/embedding
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
soundcloud: {
|
||||||
|
old_player: true // enables classic player
|
||||||
|
},
|
||||||
|
giphy: {
|
||||||
|
media_only: true // disables branded player for gifs and returns just the image
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
bandcamp: {
|
||||||
|
get_params: '/size=large/bgcol=333333/linkcol=ffffff/artwork=small/transparent=true/',
|
||||||
|
media: {
|
||||||
|
album: {
|
||||||
|
height: 472,
|
||||||
|
'max-width': 700
|
||||||
|
},
|
||||||
|
track: {
|
||||||
|
height: 120,
|
||||||
|
'max-width': 700
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
|
||||||
|
// WHITELIST_WILDCARD, if present, will be added to whitelist as record for top level domain: "*"
|
||||||
|
// with it, you can define what parsers do when they run accross unknown publisher.
|
||||||
|
// If absent or empty, all generic media parsers will be disabled except for known domains
|
||||||
|
// More about format: https://iframely.com/docs/qa-format
|
||||||
|
|
||||||
|
/*
|
||||||
|
WHITELIST_WILDCARD: {
|
||||||
|
"twitter": {
|
||||||
|
"player": "allow",
|
||||||
|
"photo": "deny"
|
||||||
|
},
|
||||||
|
"oembed": {
|
||||||
|
"video": "allow",
|
||||||
|
"photo": "allow",
|
||||||
|
"rich": "deny",
|
||||||
|
"link": "deny"
|
||||||
|
},
|
||||||
|
"og": {
|
||||||
|
"video": ["allow", "ssl", "responsive"]
|
||||||
|
},
|
||||||
|
"iframely": {
|
||||||
|
"survey": "allow",
|
||||||
|
"reader": "allow",
|
||||||
|
"player": "allow",
|
||||||
|
"image": "allow"
|
||||||
|
},
|
||||||
|
"html-meta": {
|
||||||
|
"video": ["allow", "responsive"],
|
||||||
|
"promo": "allow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Black-list any of the inappropriate domains. Iframely will return 417
|
||||||
|
// At minimum, keep your localhosts blacklisted to avoid SSRF
|
||||||
|
BLACKLIST_DOMAINS_RE: [
|
||||||
|
/^https?:\/\/127\.0\.0\.1/i,
|
||||||
|
/^https?:\/\/localhost/i,
|
||||||
|
|
||||||
|
// And this is AWS metadata service
|
||||||
|
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
|
||||||
|
/^https?:\/\/169\.254\.169\.254/
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
|
})();
|
8
docker/prod/docker-compose.yml
vendored
8
docker/prod/docker-compose.yml
vendored
|
@ -26,6 +26,14 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- lemmy_pictshare:/usr/share/nginx/html/data
|
- lemmy_pictshare:/usr/share/nginx/html/data
|
||||||
restart: always
|
restart: always
|
||||||
|
lemmy_iframely:
|
||||||
|
image: dogbin/iframely:latest
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8061:8061"
|
||||||
|
volumes:
|
||||||
|
- ./iframely.config.local.js:/iframely/config.local.js:ro
|
||||||
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
lemmy_db:
|
lemmy_db:
|
||||||
lemmy_pictshare:
|
lemmy_pictshare:
|
||||||
|
lemmy_iframely:
|
||||||
|
|
1
docs/src/administration_install_docker.md
vendored
1
docs/src/administration_install_docker.md
vendored
|
@ -7,6 +7,7 @@ mkdir lemmy/
|
||||||
cd lemmy/
|
cd lemmy/
|
||||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
||||||
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/lemmy.hjson
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/lemmy.hjson
|
||||||
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/iframely.config.local.js
|
||||||
# Edit lemmy.hjson, and docker-compose.yml to do more configuration (like adding a custom password)
|
# Edit lemmy.hjson, and docker-compose.yml to do more configuration (like adding a custom password)
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
100
ui/src/components/iframely-card.tsx
vendored
Normal file
100
ui/src/components/iframely-card.tsx
vendored
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import { Component, linkEvent } from 'inferno';
|
||||||
|
import { FramelyData } from '../interfaces';
|
||||||
|
import { mdToHtml } from '../utils';
|
||||||
|
|
||||||
|
interface FramelyCardProps {
|
||||||
|
iframely: FramelyData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FramelyCardState {
|
||||||
|
expanded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IFramelyCard extends Component<
|
||||||
|
FramelyCardProps,
|
||||||
|
FramelyCardState
|
||||||
|
> {
|
||||||
|
private emptyState: FramelyCardState = {
|
||||||
|
expanded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: any, context: any) {
|
||||||
|
super(props, context);
|
||||||
|
this.state = this.emptyState;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let iframely = this.props.iframely;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="card my-2">
|
||||||
|
<div class="row no-gutters">
|
||||||
|
{iframely.thumbnail_url && (
|
||||||
|
<div class="col-sm-3">
|
||||||
|
{iframely.html ? (
|
||||||
|
<span
|
||||||
|
class="pointer"
|
||||||
|
onClick={linkEvent(this, this.handleIframeExpand)}
|
||||||
|
>
|
||||||
|
<img class="card-img" src={iframely.thumbnail_url} />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
class="img-fluid card-img"
|
||||||
|
src={iframely.thumbnail_url}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title d-inline">
|
||||||
|
<span>
|
||||||
|
<a class="text-body" target="_blank" href={iframely.url}>
|
||||||
|
{iframely.title}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</h5>
|
||||||
|
<span class="d-inline-block ml-2 mb-2 small text-muted">
|
||||||
|
<a class="text-muted" target="_blank" href={iframely.url}>
|
||||||
|
{new URL(iframely.url).hostname}
|
||||||
|
<svg class="ml-1 icon">
|
||||||
|
<use xlinkHref="#icon-external-link"></use>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
{iframely.html && (
|
||||||
|
<span
|
||||||
|
class="ml-2 pointer"
|
||||||
|
onClick={linkEvent(this, this.handleIframeExpand)}
|
||||||
|
>
|
||||||
|
{this.state.expanded ? '[-]' : '[+]'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{iframely.description && (
|
||||||
|
<div
|
||||||
|
className="card-text small text-muted md-div"
|
||||||
|
dangerouslySetInnerHTML={mdToHtml(iframely.description)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{this.state.expanded && (
|
||||||
|
<div class="my-2 embed-responsive embed-responsive-16by9">
|
||||||
|
<div
|
||||||
|
class="embed-responsive-item"
|
||||||
|
dangerouslySetInnerHTML={{ __html: iframely.html }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleIframeExpand(i: IFramelyCard) {
|
||||||
|
i.state.expanded = !i.state.expanded;
|
||||||
|
i.setState(i.state);
|
||||||
|
}
|
||||||
|
}
|
53
ui/src/components/post-listing.tsx
vendored
53
ui/src/components/post-listing.tsx
vendored
|
@ -15,9 +15,11 @@ import {
|
||||||
AddAdminForm,
|
AddAdminForm,
|
||||||
TransferSiteForm,
|
TransferSiteForm,
|
||||||
TransferCommunityForm,
|
TransferCommunityForm,
|
||||||
|
FramelyData,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { MomentTime } from './moment-time';
|
import { MomentTime } from './moment-time';
|
||||||
import { PostForm } from './post-form';
|
import { PostForm } from './post-form';
|
||||||
|
import { IFramelyCard } from './iframely-card';
|
||||||
import {
|
import {
|
||||||
mdToHtml,
|
mdToHtml,
|
||||||
canMod,
|
canMod,
|
||||||
|
@ -47,6 +49,7 @@ interface PostListingState {
|
||||||
score: number;
|
score: number;
|
||||||
upvotes: number;
|
upvotes: number;
|
||||||
downvotes: number;
|
downvotes: number;
|
||||||
|
iframely: FramelyData;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PostListingProps {
|
interface PostListingProps {
|
||||||
|
@ -74,6 +77,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
score: this.props.post.score,
|
score: this.props.post.score,
|
||||||
upvotes: this.props.post.upvotes,
|
upvotes: this.props.post.upvotes,
|
||||||
downvotes: this.props.post.downvotes,
|
downvotes: this.props.post.downvotes,
|
||||||
|
iframely: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: any, context: any) {
|
constructor(props: any, context: any) {
|
||||||
|
@ -84,6 +88,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
this.handlePostDisLike = this.handlePostDisLike.bind(this);
|
this.handlePostDisLike = this.handlePostDisLike.bind(this);
|
||||||
this.handleEditPost = this.handleEditPost.bind(this);
|
this.handleEditPost = this.handleEditPost.bind(this);
|
||||||
this.handleEditCancel = this.handleEditCancel.bind(this);
|
this.handleEditCancel = this.handleEditCancel.bind(this);
|
||||||
|
|
||||||
|
if (this.props.post.url) {
|
||||||
|
this.fetchIframely();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps: PostListingProps) {
|
componentWillReceiveProps(nextProps: PostListingProps) {
|
||||||
|
@ -141,7 +149,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{post.url && isImage(post.url) && !this.state.imageExpanded && (
|
{this.hasImage() && !this.state.imageExpanded && (
|
||||||
<span
|
<span
|
||||||
title={i18n.t('expand_here')}
|
title={i18n.t('expand_here')}
|
||||||
class="pointer"
|
class="pointer"
|
||||||
|
@ -151,7 +159,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
className={`mx-2 mt-1 float-left img-fluid thumbnail rounded ${(post.nsfw ||
|
className={`mx-2 mt-1 float-left img-fluid thumbnail rounded ${(post.nsfw ||
|
||||||
post.community_nsfw) &&
|
post.community_nsfw) &&
|
||||||
'img-blur'}`}
|
'img-blur'}`}
|
||||||
src={imageThumbnailer(post.url)}
|
src={imageThumbnailer(this.getImage())}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
@ -205,7 +213,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</a>
|
</a>
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
{post.url && isImage(post.url) && (
|
{this.hasImage() && (
|
||||||
<>
|
<>
|
||||||
{!this.state.imageExpanded ? (
|
{!this.state.imageExpanded ? (
|
||||||
<span
|
<span
|
||||||
|
@ -228,7 +236,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
class="pointer"
|
class="pointer"
|
||||||
onClick={linkEvent(this, this.handleImageExpandClick)}
|
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||||
>
|
>
|
||||||
<img class="img-fluid img-expanded" src={post.url} />
|
<img
|
||||||
|
class="img-fluid img-expanded"
|
||||||
|
src={this.getImage()}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
@ -587,6 +598,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
{post.url && this.props.showBody && this.state.iframely && (
|
||||||
|
<IFramelyCard iframely={this.state.iframely} />
|
||||||
|
)}
|
||||||
{this.state.showRemoveDialog && (
|
{this.state.showRemoveDialog && (
|
||||||
<form
|
<form
|
||||||
class="form-inline"
|
class="form-inline"
|
||||||
|
@ -737,6 +751,37 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetchIframely() {
|
||||||
|
fetch(`/iframely/oembed?url=${this.props.post.url}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(res => {
|
||||||
|
this.state.iframely = res;
|
||||||
|
this.setState(this.state);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(`Iframely service not set up properly. ${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hasImage(): boolean {
|
||||||
|
return (
|
||||||
|
(this.props.post.url && isImage(this.props.post.url)) ||
|
||||||
|
(this.state.iframely && this.state.iframely.thumbnail_url !== undefined)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getImage(): string {
|
||||||
|
let simpleImg = isImage(this.props.post.url);
|
||||||
|
if (simpleImg) {
|
||||||
|
return this.props.post.url;
|
||||||
|
} else if (this.state.iframely) {
|
||||||
|
let iframelyThumbnail = this.state.iframely.thumbnail_url;
|
||||||
|
if (iframelyThumbnail) {
|
||||||
|
return iframelyThumbnail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handlePostLike(i: PostListing) {
|
handlePostLike(i: PostListing) {
|
||||||
let new_vote = i.state.my_vote == 1 ? 0 : 1;
|
let new_vote = i.state.my_vote == 1 ? 0 : 1;
|
||||||
|
|
||||||
|
|
15
ui/src/interfaces.ts
vendored
15
ui/src/interfaces.ts
vendored
|
@ -876,3 +876,18 @@ export interface WebSocketJsonResponse {
|
||||||
error?: string;
|
error?: string;
|
||||||
reconnect?: boolean;
|
reconnect?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FramelyData {
|
||||||
|
url: string;
|
||||||
|
type: string;
|
||||||
|
version?: string;
|
||||||
|
title: string;
|
||||||
|
author?: string;
|
||||||
|
author_url?: string;
|
||||||
|
provider_name?: string;
|
||||||
|
thumbnail_url?: string;
|
||||||
|
thumbnail_width?: number;
|
||||||
|
thumbnail_height?: number;
|
||||||
|
description?: string;
|
||||||
|
html?: string;
|
||||||
|
}
|
||||||
|
|
Reference in a new issue