fix #361 with rich OEmbed display via iframe, fix #237 by hiding sensitive content behind a spoiler on public pagesmaster
@@ -1,8 +1,20 @@ | |||
import emojify from './components/emoji' | |||
$(() => { | |||
$.each($('.entry .content, .name, .account__header__content'), (_, content) => { | |||
$.each($('.entry .content, .entry .status__content, .display-name, .name, .account__header__content'), (_, content) => { | |||
const $content = $(content); | |||
$content.html(emojify($content.html())); | |||
}); | |||
$('.video-player video').on('click', e => { | |||
if (e.target.paused) { | |||
e.target.play(); | |||
} else { | |||
e.target.pause(); | |||
} | |||
}); | |||
$('.media-spoiler').on('click', e => { | |||
$(e.target).hide(); | |||
}); | |||
}); |
@@ -3,232 +3,281 @@ | |||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); | |||
.entry { | |||
border-bottom: 1px solid #d9e1e8; | |||
background: #fff; | |||
border-left: 2px solid #fff; | |||
.status.light, .detailed-status.light { | |||
border-bottom: 1px solid #d9e1e8; | |||
} | |||
&.entry-reblog { | |||
border-left-color: #2b90d9; | |||
&:last-child { | |||
.status.light, .detailed-status.light { | |||
border-bottom: 0; | |||
border-radius: 0 0 4px 4px; | |||
} | |||
} | |||
&.entry-predecessor, &.entry-successor { | |||
background: #d9e1e8; | |||
border-left-color: #d9e1e8; | |||
border-bottom-color: darken(#d9e1e8, 10%); | |||
&:first-child { | |||
.status.light, .detailed-status.light { | |||
border-radius: 4px 4px 0 0; | |||
} | |||
.header { | |||
.header__right { | |||
.counter-btn { | |||
color: darken(#d9e1e8, 15%); | |||
} | |||
&:last-child { | |||
.status.light, .detailed-status.light { | |||
border-radius: 4px; | |||
} | |||
} | |||
} | |||
} | |||
&.entry-center { | |||
border-bottom-color: darken(#d9e1e8, 10%); | |||
} | |||
.status.light { | |||
padding: 14px 14px 14px (48px + 14px*2); | |||
position: relative; | |||
min-height: 48px; | |||
cursor: default; | |||
background: lighten(#d9e1e8, 8%); | |||
&.entry-follow, &.entry-favourite { | |||
.content { | |||
padding-top: 10px; | |||
padding-bottom: 10px; | |||
.status__header { | |||
font-size: 15px; | |||
strong { | |||
font-weight: 500; | |||
.status__meta { | |||
float: right; | |||
font-size: 14px; | |||
.status__relative-time { | |||
color: #9baec8; | |||
} | |||
} | |||
} | |||
&:last-child { | |||
border-bottom: 0; | |||
border-radius: 0 0 4px 4px; | |||
.status__display-name { | |||
display: block; | |||
max-width: 100%; | |||
padding-right: 25px; | |||
color: #282c37; | |||
} | |||
} | |||
.entry:first-child { | |||
border-radius: 4px 4px 0 0; | |||
.status__avatar { | |||
position: absolute; | |||
left: 14px; | |||
top: 14px; | |||
width: 48px; | |||
height: 48px; | |||
&:last-child { | |||
border-radius: 4px; | |||
& > div { | |||
width: 48px; | |||
height: 48px; | |||
} | |||
img { | |||
display: block; | |||
border-radius: 4px; | |||
} | |||
} | |||
} | |||
@media screen and (max-width: 700px) { | |||
border-radius: 0; | |||
box-shadow: none; | |||
.display-name { | |||
display: block; | |||
max-width: 100%; | |||
overflow: hidden; | |||
white-space: nowrap; | |||
text-overflow: ellipsis; | |||
.entry { | |||
&:last-child { | |||
border-radius: 0; | |||
strong { | |||
font-weight: 500; | |||
color: #282c37; | |||
} | |||
&:first-child { | |||
border-radius: 0; | |||
span { | |||
font-size: 14px; | |||
color: #9baec8; | |||
} | |||
} | |||
&:last-child { | |||
border-radius: 0; | |||
} | |||
.status__content { | |||
color: #282c37; | |||
a { | |||
color: #2b90d9; | |||
} | |||
} | |||
} | |||
.entry__container { | |||
overflow: hidden; | |||
.status__attachments { | |||
margin-top: 8px; | |||
overflow: hidden; | |||
width: 100%; | |||
box-sizing: border-box; | |||
height: 110px; | |||
display: flex; | |||
} | |||
} | |||
.avatar { | |||
width: 56px; | |||
padding: 15px 10px; | |||
padding-right: 5px; | |||
float: left; | |||
.detailed-status.light { | |||
padding: 14px; | |||
background: #fff; | |||
cursor: default; | |||
img { | |||
width: 56px; | |||
height: 56px; | |||
.detailed-status__display-name { | |||
display: block; | |||
border-radius: 4px; | |||
} | |||
} | |||
overflow: hidden; | |||
margin-bottom: 15px; | |||
.entry__container__container { | |||
margin-left: 71px; | |||
} | |||
& > div { | |||
float: left; | |||
margin-right: 10px; | |||
} | |||
.header { | |||
margin-bottom: 10px; | |||
padding: 15px; | |||
padding-bottom: 0; | |||
padding-left: 8px; | |||
display: flex; | |||
.display-name { | |||
display: block; | |||
max-width: 100%; | |||
overflow: hidden; | |||
white-space: nowrap; | |||
text-overflow: ellipsis; | |||
strong { | |||
font-weight: 500; | |||
color: #282c37; | |||
} | |||
.header__left { | |||
flex: 1; | |||
span { | |||
font-size: 14px; | |||
color: #9baec8; | |||
} | |||
} | |||
} | |||
.header__right { | |||
.avatar { | |||
width: 48px; | |||
height: 48px; | |||
img { | |||
display: block; | |||
border-radius: 4px; | |||
} | |||
} | |||
.name { | |||
text-decoration: none; | |||
.status__content { | |||
color: #282c37; | |||
a { | |||
color: #2b90d9; | |||
} | |||
} | |||
.detailed-status__meta { | |||
margin-top: 15px; | |||
color: #9baec8; | |||
font-size: 14px; | |||
line-height: 18px; | |||
strong { | |||
color: #282c37; | |||
font-weight: 500; | |||
a { | |||
color: inherit; | |||
} | |||
&:hover { | |||
strong { | |||
text-decoration: underline; | |||
} | |||
span > span { | |||
font-weight: 500; | |||
font-size: 12px; | |||
margin-left: 6px; | |||
display: inline-block; | |||
} | |||
} | |||
} | |||
.pre-header { | |||
border-bottom: 1px solid #d9e1e8; | |||
color: #2b90d9; | |||
padding: 5px 10px; | |||
padding-left: 8px; | |||
clear: both; | |||
.name { | |||
color: #2b90d9; | |||
font-weight: 500; | |||
text-decoration: none; | |||
.detailed-status__attachments { | |||
margin-top: 8px; | |||
overflow: hidden; | |||
width: 100%; | |||
box-sizing: border-box; | |||
height: 300px; | |||
display: flex; | |||
} | |||
&:hover { | |||
text-decoration: underline; | |||
.video-player { | |||
margin-top: 8px; | |||
height: 300px; | |||
overflow: hidden; | |||
video { | |||
position: relative; | |||
z-index: 1; | |||
width: 100%; | |||
height: 100%; | |||
object-fit: cover; | |||
top: 50%; | |||
transform: translateY(-50%); | |||
} | |||
} | |||
} | |||
.content { | |||
font-size: 14px; | |||
padding: 0 15px; | |||
padding-left: 8px; | |||
padding-bottom: 15px; | |||
color: #282c37; | |||
word-wrap: break-word; | |||
overflow: hidden; | |||
white-space: pre-wrap; | |||
p { | |||
margin-bottom: 18px; | |||
.media-item, .video-item { | |||
box-sizing: border-box; | |||
position: relative; | |||
left: auto; | |||
top: auto; | |||
right: auto; | |||
bottom: auto; | |||
float: left; | |||
border: medium none; | |||
display: block; | |||
flex: 1 1 auto; | |||
height: 100%; | |||
margin-right: 2px; | |||
&:last-child { | |||
margin-bottom: 0; | |||
} | |||
&:last-child { | |||
margin-right: 0; | |||
} | |||
a { | |||
color: #2b90d9; | |||
display: block; | |||
width: 100%; | |||
height: 100%; | |||
background: no-repeat scroll center center / cover; | |||
text-decoration: none; | |||
cursor: zoom-in; | |||
} | |||
} | |||
&:hover { | |||
text-decoration: underline; | |||
} | |||
.video-item { | |||
max-width: 196px; | |||
&.mention { | |||
&:hover { | |||
text-decoration: none; | |||
a { | |||
cursor: pointer; | |||
} | |||
span { | |||
text-decoration: underline; | |||
} | |||
} | |||
} | |||
.video-item__play { | |||
position: absolute; | |||
top: 50%; | |||
left: 50%; | |||
font-size: 36px; | |||
transform: translate(-50%, -50%); | |||
padding: 5px; | |||
border-radius: 100px; | |||
color: rgba(255, 255, 255, 0.8); | |||
} | |||
} | |||
.time { | |||
text-decoration: none; | |||
color: #9baec8; | |||
.media-spoiler { | |||
background: #9baec8; | |||
width: 100%; | |||
height: 100%; | |||
cursor: pointer; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
flex-direction: column; | |||
text-align: center; | |||
transition: all 100ms linear; | |||
&:hover { | |||
text-decoration: underline; | |||
background: darken(#9baec8, 5%); | |||
} | |||
} | |||
.media-attachments { | |||
list-style: none; | |||
margin: 0; | |||
padding: 0; | |||
display: block; | |||
overflow: hidden; | |||
padding-left: 10px; | |||
margin-bottom: 15px; | |||
li { | |||
span { | |||
display: block; | |||
float: left; | |||
width: 120px; | |||
height: 100px; | |||
border-radius: 4px; | |||
margin-right: 4px; | |||
margin-bottom: 4px; | |||
a { | |||
display: block; | |||
width: 120px; | |||
height: 100px; | |||
border-radius: 4px; | |||
background-position: center; | |||
background-repeat: none; | |||
background-size: cover; | |||
&:first-child { | |||
font-size: 14px; | |||
} | |||
} | |||
} | |||
@media screen and (max-width: 360px) { | |||
.avatar { | |||
display: none; | |||
} | |||
.entry__container__container { | |||
margin-left: 7px; | |||
&:last-child { | |||
font-size: 11px; | |||
font-weight: 500; | |||
} | |||
} | |||
} | |||
} | |||
@@ -5,8 +5,8 @@ class Api::OembedController < ApiController | |||
def show | |||
@stream_entry = stream_entry_from_url(params[:url]) | |||
@width = [300, params[:maxwidth].to_i].max | |||
@height = [200, params[:maxheight].to_i].max | |||
@width = params[:maxwidth].present? ? params[:maxwidth].to_i : 400 | |||
@height = params[:maxheight].present? ? params[:maxheight].to_i : 600 | |||
end | |||
private | |||
@@ -9,8 +9,6 @@ class StreamEntriesController < ApplicationController | |||
before_action :check_account_suspension | |||
def show | |||
@type = @stream_entry.activity_type.downcase | |||
respond_to do |format| | |||
format.html do | |||
return gone if @stream_entry.activity.nil? | |||
@@ -27,7 +25,7 @@ class StreamEntriesController < ApplicationController | |||
def embed | |||
response.headers['X-Frame-Options'] = 'ALLOWALL' | |||
@type = @stream_entry.activity_type.downcase | |||
@external_links = true | |||
return gone if @stream_entry.activity.nil? | |||
@@ -46,6 +44,7 @@ class StreamEntriesController < ApplicationController | |||
def set_stream_entry | |||
@stream_entry = @account.stream_entries.find(params[:id]) | |||
@type = @stream_entry.activity_type.downcase | |||
end | |||
def check_account_suspension | |||
@@ -5,6 +5,10 @@ module StreamEntriesHelper | |||
account.display_name.blank? ? account.username : account.display_name | |||
end | |||
def acct(account) | |||
"@#{account.acct}#{@external_links && account.local? ? "@#{Rails.configuration.x.local_domain}" : ''}" | |||
end | |||
def avatar_for_status_url(status) | |||
status.reblog? ? status.reblog.account.avatar.url( :original) : status.account.avatar.url( :original) | |||
end | |||
@@ -9,6 +9,6 @@ node(:author_url) { |entry| account_url(entry.account) } | |||
node(:provider_name) { Rails.configuration.x.local_domain } | |||
node(:provider_url) { root_url } | |||
node(:cache_age) { 86_400 } | |||
node(:html) { |entry| "<div style=\"position: relative; height: 0; overflow: hidden; padding-top: 30px; padding-bottom: 56.25%\"><iframe src=\"#{embed_account_stream_entry_url(entry.account, entry)}\" style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden\" frameborder=\"0\" width=\"#{@width}\" scrolling=\"no\"></iframe></div>" } | |||
node(:html) { |entry| "<iframe src=\"#{embed_account_stream_entry_url(entry.account, entry)}\" style=\"width: 100%; overflow: hidden\" frameborder=\"0\" width=\"#{@width}\" height=\"#{@height}\" scrolling=\"no\"></iframe>" } | |||
node(:width) { @width } | |||
node(:height) { nil } |
@@ -0,0 +1,3 @@ | |||
.media-spoiler | |||
%span= t('stream_entries.sensitive_content') | |||
%span= t('stream_entries.click_to_show') |
@@ -0,0 +1,36 @@ | |||
.detailed-status.light | |||
= link_to TagManager.instance.url_for(status.account), class: 'detailed-status__display-name', target: @external_links ? '_blank' : nil, rel: 'noopener' do | |||
%div | |||
%div.avatar | |||
= image_tag status.account.avatar.url(:original), width: 48, height: 48, alt: '' | |||
%span.display-name | |||
%strong= display_name(status.account) | |||
%span= acct(status.account) | |||
.status__content= Formatter.instance.format(status) | |||
- unless status.media_attachments.empty? | |||
- if status.media_attachments.first.video? | |||
.video-player | |||
- if status.sensitive? | |||
= render partial: 'stream_entries/content_spoiler' | |||
%video{ src: status.media_attachments.first.file.url(:original), loop: true } | |||
- else | |||
.detailed-status__attachments | |||
- if status.sensitive? | |||
= render partial: 'stream_entries/content_spoiler' | |||
- status.media_attachments.each do |media| | |||
.media-item | |||
= link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener' | |||
%div.detailed-status__meta | |||
= link_to TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: @external_links ? '_blank' : nil, rel: 'noopener' do | |||
%span= l(status.created_at) | |||
· | |||
%span | |||
= fa_icon('retweet') | |||
%span= status.reblogs.count | |||
· | |||
%span | |||
= fa_icon('star') | |||
%span= status.favourites.count |
@@ -0,0 +1,28 @@ | |||
.status.light | |||
.status__header | |||
.status__meta | |||
= link_to time_ago_in_words(status.created_at), TagManager.instance.url_for(status), class: 'status__relative-time', title: l(status.created_at), target: @external_links ? '_blank' : nil, rel: 'noopener' | |||
= link_to TagManager.instance.url_for(status.account), class: 'status__display-name', target: @external_links ? '_blank' : nil, rel: 'noopener' do | |||
.status__avatar | |||
%div | |||
= image_tag status.account.avatar(:original), width: 48, height: 48, alt: '' | |||
%span.display-name | |||
%strong= display_name(status.account) | |||
%span= acct(status.account) | |||
.status__content= Formatter.instance.format(status) | |||
- unless status.media_attachments.empty? | |||
.status__attachments | |||
- if status.sensitive? | |||
= render partial: 'stream_entries/content_spoiler' | |||
- if status.media_attachments.first.video? | |||
.video-item | |||
= link_to (status.media_attachments.first.remote_url.blank? ? status.media_attachments.first.file.url(:original) : status.media_attachments.first.remote_url), style: "background-image: url(#{status.media_attachments.first.file.url(:small)})", target: '_blank', rel: 'noopener' do | |||
.video-item__play | |||
= fa_icon('play') | |||
- else | |||
- status.media_attachments.each do |media| | |||
.media-item | |||
= link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener' |
@@ -1,7 +1,7 @@ | |||
- include_threads ||= false | |||
- is_predecessor ||= false | |||
- is_successor ||= false | |||
- centered = include_threads && !is_predecessor && !is_successor | |||
- centered ||= include_threads && !is_predecessor && !is_successor | |||
- if status.reply? && include_threads | |||
= render partial: 'status', collection: @ancestors, as: :status, locals: { is_predecessor: true } | |||
@@ -13,28 +13,7 @@ | |||
Shared by | |||
= link_to display_name(status.account), TagManager.instance.url_for(status.account), class: 'name' | |||
.entry__container | |||
.avatar | |||
= image_tag avatar_for_status_url(status) | |||
.entry__container__container | |||
.header | |||
.header__left | |||
= link_to TagManager.instance.url_for(proper_status(status).account), class: 'name' do | |||
%strong= display_name(proper_status(status).account) | |||
= "@#{proper_status(status).account.acct}" | |||
.header__right | |||
= link_to TagManager.instance.url_for(proper_status(status)), class: 'time' do | |||
%span{ title: proper_status(status).created_at } | |||
= relative_time(proper_status(status).created_at) | |||
.content= Formatter.instance.format(proper_status(status)) | |||
- if (status.reblog? ? status.reblog : status).media_attachments.size > 0 | |||
%ul.media-attachments | |||
- (status.reblog? ? status.reblog : status).media_attachments.each do |media| | |||
%li.transparent-background= link_to '', media.file.url( :original), style: "background-image: url(#{media.file.url( :small)})", target: '_blank' | |||
= render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: proper_status(status) } | |||
- if include_threads | |||
= render partial: 'status', collection: @descendants, as: :status, locals: { is_successor: true } |
@@ -1,2 +1,2 @@ | |||
.activity-stream.activity-stream-headless | |||
= render partial: @type, locals: { @type.to_sym => @stream_entry.activity } | |||
= render partial: @type, locals: { @type.to_sym => @stream_entry.activity, centered: true } |
@@ -33,6 +33,7 @@ search: | |||
ignore_unused: | |||
- 'activerecord.attributes.*' | |||
- '{devise,will_paginate,doorkeeper}.*' | |||
- '{datetime,time}.*' | |||
- 'simple_form.{yes,no}' | |||
- 'simple_form.{placeholders,hints,labels}.*' | |||
- 'simple_form.{error_notification,required}.:' | |||
@@ -26,6 +26,20 @@ en: | |||
resend_confirmation: Resend confirmation instructions | |||
reset_password: Reset password | |||
set_new_password: Set new password | |||
datetime: | |||
distance_in_words: | |||
about_x_hours: "%{count}h" | |||
about_x_months: "%{count}mo" | |||
about_x_years: "%{count}y" | |||
almost_x_years: "%{count}y" | |||
half_a_minute: Just now | |||
less_than_x_minutes: "%{count}m" | |||
less_than_x_seconds: Just now | |||
over_x_years: "%{count}y" | |||
x_days: "%{count}d" | |||
x_minutes: "%{count}m" | |||
x_months: "%{count}mo" | |||
x_seconds: "%{count}s" | |||
generic: | |||
changes_saved_msg: Changes successfully saved! | |||
powered_by: powered by %{link} | |||
@@ -53,8 +67,13 @@ en: | |||
edit_profile: Edit profile | |||
preferences: Preferences | |||
stream_entries: | |||
click_to_show: Click to show | |||
favourited: favourited a post by | |||
is_now_following: is now following | |||
sensitive_content: Sensitive content | |||
time: | |||
formats: | |||
default: "%b %d, %Y, %H:%M" | |||
users: | |||
invalid_email: The e-mail address is invalid | |||
will_paginate: | |||