Adjusting public display of statuses to look similar to logged-in UI,

fix #361 with rich OEmbed display via iframe, fix #237 by hiding sensitive
content behind a spoiler on public pages
lolsob-rspec
Eugen Rochko 2016-12-18 19:47:11 +01:00
parent 6d7f3be2f6
commit 346aae50b0
13 changed files with 339 additions and 209 deletions

View File

@ -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();
});
});

View File

@ -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;
&.entry-reblog {
border-left-color: #2b90d9;
.status.light, .detailed-status.light {
border-bottom: 1px solid #d9e1e8;
}
&.entry-predecessor, &.entry-successor {
background: #d9e1e8;
border-left-color: #d9e1e8;
border-bottom-color: darken(#d9e1e8, 10%);
&:last-child {
.status.light, .detailed-status.light {
border-bottom: 0;
border-radius: 0 0 4px 4px;
}
}
.header {
.header__right {
.counter-btn {
color: darken(#d9e1e8, 15%);
}
&:first-child {
.status.light, .detailed-status.light {
border-radius: 4px 4px 0 0;
}
&:last-child {
.status.light, .detailed-status.light {
border-radius: 4px;
}
}
}
}
.status.light {
padding: 14px 14px 14px (48px + 14px*2);
position: relative;
min-height: 48px;
cursor: default;
background: lighten(#d9e1e8, 8%);
.status__header {
font-size: 15px;
.status__meta {
float: right;
font-size: 14px;
.status__relative-time {
color: #9baec8;
}
}
}
&.entry-center {
border-bottom-color: darken(#d9e1e8, 10%);
.status__display-name {
display: block;
max-width: 100%;
padding-right: 25px;
color: #282c37;
}
&.entry-follow, &.entry-favourite {
.content {
padding-top: 10px;
padding-bottom: 10px;
.status__avatar {
position: absolute;
left: 14px;
top: 14px;
width: 48px;
height: 48px;
& > div {
width: 48px;
height: 48px;
}
img {
display: block;
border-radius: 4px;
}
}
.display-name {
display: block;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
strong {
font-weight: 500;
color: #282c37;
}
span {
font-size: 14px;
color: #9baec8;
}
}
.status__content {
color: #282c37;
a {
color: #2b90d9;
}
}
.status__attachments {
margin-top: 8px;
overflow: hidden;
width: 100%;
box-sizing: border-box;
height: 110px;
display: flex;
}
}
.detailed-status.light {
padding: 14px;
background: #fff;
cursor: default;
.detailed-status__display-name {
display: block;
overflow: hidden;
margin-bottom: 15px;
& > div {
float: left;
margin-right: 10px;
}
.display-name {
display: block;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
strong {
font-weight: 500;
color: #282c37;
}
span {
font-size: 14px;
color: #9baec8;
}
}
}
&:last-child {
border-bottom: 0;
border-radius: 0 0 4px 4px;
}
}
.avatar {
width: 48px;
height: 48px;
.entry:first-child {
border-radius: 4px 4px 0 0;
&:last-child {
border-radius: 4px;
}
}
@media screen and (max-width: 700px) {
border-radius: 0;
box-shadow: none;
.entry {
&:last-child {
border-radius: 0;
}
&:first-child {
border-radius: 0;
&:last-child {
border-radius: 0;
}
img {
display: block;
border-radius: 4px;
}
}
}
.entry__container {
overflow: hidden;
}
.status__content {
color: #282c37;
.avatar {
width: 56px;
padding: 15px 10px;
padding-right: 5px;
float: left;
img {
width: 56px;
height: 56px;
display: block;
border-radius: 4px;
}
}
.entry__container__container {
margin-left: 71px;
}
.header {
margin-bottom: 10px;
padding: 15px;
padding-bottom: 0;
padding-left: 8px;
display: flex;
.header__left {
flex: 1;
a {
color: #2b90d9;
}
}
.header__right {
}
.name {
text-decoration: none;
.detailed-status__meta {
margin-top: 15px;
color: #9baec8;
font-size: 14px;
line-height: 18px;
strong {
color: #282c37;
a {
color: inherit;
}
span > span {
font-weight: 500;
font-size: 12px;
margin-left: 6px;
display: inline-block;
}
}
&:hover {
strong {
text-decoration: underline;
}
.detailed-status__attachments {
margin-top: 8px;
overflow: hidden;
width: 100%;
box-sizing: border-box;
height: 300px;
display: flex;
}
.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%);
}
}
}
.pre-header {
border-bottom: 1px solid #d9e1e8;
color: #2b90d9;
padding: 5px 10px;
padding-left: 8px;
clear: both;
.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;
.name {
color: #2b90d9;
font-weight: 500;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
.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;
&: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;
&:hover {
text-decoration: underline;
}
&.mention {
&:hover {
text-decoration: none;
span {
text-decoration: underline;
}
}
}
cursor: zoom-in;
}
}
.time {
text-decoration: none;
color: #9baec8;
.video-item {
max-width: 196px;
a {
cursor: pointer;
}
.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);
}
}
.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;
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -0,0 +1,3 @@
.media-spoiler
%span= t('stream_entries.sensitive_content')
%span= t('stream_entries.click_to_show')

View File

@ -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

View File

@ -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'

View File

@ -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 }

View File

@ -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 }

View File

@ -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}.:'

View File

@ -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: