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
master
Eugen Rochko 8 years ago
parent aed25932b5
commit 5ae1b39ec9
  1. 14
      app/assets/javascripts/extras.jsx
  2. 337
      app/assets/stylesheets/stream_entries.scss
  3. 4
      app/controllers/api/oembed_controller.rb
  4. 5
      app/controllers/stream_entries_controller.rb
  5. 4
      app/helpers/stream_entries_helper.rb
  6. 2
      app/views/api/oembed/show.json.rabl
  7. 3
      app/views/stream_entries/_content_spoiler.html.haml
  8. 36
      app/views/stream_entries/_detailed_status.html.haml
  9. 28
      app/views/stream_entries/_simple_status.html.haml
  10. 25
      app/views/stream_entries/_status.html.haml
  11. 2
      app/views/stream_entries/embed.html.haml
  12. 1
      config/i18n-tasks.yml
  13. 19
      config/locales/en.yml

@ -1,8 +1,20 @@
import emojify from './components/emoji' 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); const $content = $(content);
$content.html(emojify($content.html())); $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); box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
.entry { .entry {
.status.light, .detailed-status.light {
border-bottom: 1px solid #d9e1e8; border-bottom: 1px solid #d9e1e8;
background: #fff;
border-left: 2px solid #fff;
&.entry-reblog {
border-left-color: #2b90d9;
}
&.entry-predecessor, &.entry-successor {
background: #d9e1e8;
border-left-color: #d9e1e8;
border-bottom-color: darken(#d9e1e8, 10%);
.header {
.header__right {
.counter-btn {
color: darken(#d9e1e8, 15%);
}
}
}
}
&.entry-center {
border-bottom-color: darken(#d9e1e8, 10%);
}
&.entry-follow, &.entry-favourite {
.content {
padding-top: 10px;
padding-bottom: 10px;
strong {
font-weight: 500;
}
}
} }
&:last-child { &:last-child {
.status.light, .detailed-status.light {
border-bottom: 0; border-bottom: 0;
border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;
} }
} }
.entry:first-child { &:first-child {
.status.light, .detailed-status.light {
border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0;
}
&:last-child { &:last-child {
.status.light, .detailed-status.light {
border-radius: 4px; border-radius: 4px;
} }
} }
}
}
@media screen and (max-width: 700px) { .status.light {
border-radius: 0; padding: 14px 14px 14px (48px + 14px*2);
box-shadow: none; position: relative;
min-height: 48px;
cursor: default;
background: lighten(#d9e1e8, 8%);
.entry { .status__header {
&:last-child { font-size: 15px;
border-radius: 0;
}
&:first-child { .status__meta {
border-radius: 0; float: right;
font-size: 14px;
&:last-child { .status__relative-time {
border-radius: 0; color: #9baec8;
}
} }
} }
} }
.entry__container { .status__display-name {
overflow: hidden; display: block;
max-width: 100%;
padding-right: 25px;
color: #282c37;
} }
.avatar { .status__avatar {
width: 56px; position: absolute;
padding: 15px 10px; left: 14px;
padding-right: 5px; top: 14px;
float: left; width: 48px;
height: 48px;
& > div {
width: 48px;
height: 48px;
}
img { img {
width: 56px;
height: 56px;
display: block; display: block;
border-radius: 4px; border-radius: 4px;
} }
} }
.entry__container__container { .display-name {
margin-left: 71px; display: block;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
strong {
font-weight: 500;
color: #282c37;
} }
.header { span {
margin-bottom: 10px; font-size: 14px;
padding: 15px; color: #9baec8;
padding-bottom: 0; }
padding-left: 8px; }
display: flex;
.status__content {
color: #282c37;
a {
color: #2b90d9;
}
}
.header__left { .status__attachments {
flex: 1; margin-top: 8px;
overflow: hidden;
width: 100%;
box-sizing: border-box;
height: 110px;
display: flex;
}
} }
.header__right { .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;
} }
.name { .display-name {
text-decoration: none; display: block;
color: #9baec8; max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
strong { strong {
color: #282c37;
font-weight: 500; font-weight: 500;
color: #282c37;
} }
&:hover { span {
strong { font-size: 14px;
text-decoration: underline; color: #9baec8;
}
} }
} }
.avatar {
width: 48px;
height: 48px;
img {
display: block;
border-radius: 4px;
} }
} }
.pre-header { .status__content {
border-bottom: 1px solid #d9e1e8; color: #282c37;
color: #2b90d9;
padding: 5px 10px;
padding-left: 8px;
clear: both;
.name { a {
color: #2b90d9; color: #2b90d9;
font-weight: 500; }
text-decoration: none; }
&:hover { .detailed-status__meta {
text-decoration: underline; margin-top: 15px;
color: #9baec8;
font-size: 14px;
line-height: 18px;
a {
color: inherit;
} }
span > span {
font-weight: 500;
font-size: 12px;
margin-left: 6px;
display: inline-block;
} }
} }
.content { .detailed-status__attachments {
font-size: 14px; margin-top: 8px;
padding: 0 15px;
padding-left: 8px;
padding-bottom: 15px;
color: #282c37;
word-wrap: break-word;
overflow: hidden; overflow: hidden;
white-space: pre-wrap; width: 100%;
box-sizing: border-box;
height: 300px;
display: flex;
}
p { .video-player {
margin-bottom: 18px; margin-top: 8px;
height: 300px;
overflow: hidden;
&:last-child { video {
margin-bottom: 0; position: relative;
z-index: 1;
width: 100%;
height: 100%;
object-fit: cover;
top: 50%;
transform: translateY(-50%);
}
} }
} }
a { .media-item, .video-item {
color: #2b90d9; box-sizing: border-box;
text-decoration: none; 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;
&:hover { &:last-child {
text-decoration: underline; margin-right: 0;
} }
&.mention { a {
&:hover { display: block;
width: 100%;
height: 100%;
background: no-repeat scroll center center / cover;
text-decoration: none; text-decoration: none;
cursor: zoom-in;
span {
text-decoration: underline;
} }
} }
.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);
} }
} }
.time { .media-spoiler {
text-decoration: none; background: #9baec8;
color: #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 { &:hover {
text-decoration: underline; background: darken(#9baec8, 5%);
}
} }
.media-attachments { span {
list-style: none;
margin: 0;
padding: 0;
display: block; display: block;
overflow: hidden;
padding-left: 10px;
margin-bottom: 15px;
li { &:first-child {
display: block; font-size: 14px;
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;
}
}
} }
@media screen and (max-width: 360px) { &:last-child {
.avatar { font-size: 11px;
display: none; font-weight: 500;
} }
.entry__container__container {
margin-left: 7px;
} }
} }
} }

@ -5,8 +5,8 @@ class Api::OembedController < ApiController
def show def show
@stream_entry = stream_entry_from_url(params[:url]) @stream_entry = stream_entry_from_url(params[:url])
@width = [300, params[:maxwidth].to_i].max @width = params[:maxwidth].present? ? params[:maxwidth].to_i : 400
@height = [200, params[:maxheight].to_i].max @height = params[:maxheight].present? ? params[:maxheight].to_i : 600
end end
private private

@ -9,8 +9,6 @@ class StreamEntriesController < ApplicationController
before_action :check_account_suspension before_action :check_account_suspension
def show def show
@type = @stream_entry.activity_type.downcase
respond_to do |format| respond_to do |format|
format.html do format.html do
return gone if @stream_entry.activity.nil? return gone if @stream_entry.activity.nil?
@ -27,7 +25,7 @@ class StreamEntriesController < ApplicationController
def embed def embed
response.headers['X-Frame-Options'] = 'ALLOWALL' response.headers['X-Frame-Options'] = 'ALLOWALL'
@type = @stream_entry.activity_type.downcase @external_links = true
return gone if @stream_entry.activity.nil? return gone if @stream_entry.activity.nil?
@ -46,6 +44,7 @@ class StreamEntriesController < ApplicationController
def set_stream_entry def set_stream_entry
@stream_entry = @account.stream_entries.find(params[:id]) @stream_entry = @account.stream_entries.find(params[:id])
@type = @stream_entry.activity_type.downcase
end end
def check_account_suspension def check_account_suspension

@ -5,6 +5,10 @@ module StreamEntriesHelper
account.display_name.blank? ? account.username : account.display_name account.display_name.blank? ? account.username : account.display_name
end end
def acct(account)
"@#{account.acct}#{@external_links && account.local? ? "@#{Rails.configuration.x.local_domain}" : ''}"
end
def avatar_for_status_url(status) def avatar_for_status_url(status)
status.reblog? ? status.reblog.account.avatar.url( :original) : status.account.avatar.url( :original) status.reblog? ? status.reblog.account.avatar.url( :original) : status.account.avatar.url( :original)
end end

@ -9,6 +9,6 @@ node(:author_url) { |entry| account_url(entry.account) }
node(:provider_name) { Rails.configuration.x.local_domain } node(:provider_name) { Rails.configuration.x.local_domain }
node(:provider_url) { root_url } node(:provider_url) { root_url }
node(:cache_age) { 86_400 } 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(:width) { @width }
node(:height) { nil } 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 - include_threads ||= false
- is_predecessor ||= false - is_predecessor ||= false
- is_successor ||= false - is_successor ||= false
- centered = include_threads && !is_predecessor && !is_successor - centered ||= include_threads && !is_predecessor && !is_successor
- if status.reply? && include_threads - if status.reply? && include_threads
= render partial: 'status', collection: @ancestors, as: :status, locals: { is_predecessor: true } = render partial: 'status', collection: @ancestors, as: :status, locals: { is_predecessor: true }
@ -13,28 +13,7 @@
Shared by Shared by
= link_to display_name(status.account), TagManager.instance.url_for(status.account), class: 'name' = link_to display_name(status.account), TagManager.instance.url_for(status.account), class: 'name'
.entry__container = render partial: centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status', locals: { status: proper_status(status) }
.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'
- if include_threads - if include_threads
= render partial: 'status', collection: @descendants, as: :status, locals: { is_successor: true } = render partial: 'status', collection: @descendants, as: :status, locals: { is_successor: true }

@ -1,2 +1,2 @@
.activity-stream.activity-stream-headless .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: ignore_unused:
- 'activerecord.attributes.*' - 'activerecord.attributes.*'
- '{devise,will_paginate,doorkeeper}.*' - '{devise,will_paginate,doorkeeper}.*'
- '{datetime,time}.*'
- 'simple_form.{yes,no}' - 'simple_form.{yes,no}'
- 'simple_form.{placeholders,hints,labels}.*' - 'simple_form.{placeholders,hints,labels}.*'
- 'simple_form.{error_notification,required}.:' - 'simple_form.{error_notification,required}.:'

@ -26,6 +26,20 @@ en:
resend_confirmation: Resend confirmation instructions resend_confirmation: Resend confirmation instructions
reset_password: Reset password reset_password: Reset password
set_new_password: Set new 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: generic:
changes_saved_msg: Changes successfully saved! changes_saved_msg: Changes successfully saved!
powered_by: powered by %{link} powered_by: powered by %{link}
@ -53,8 +67,13 @@ en:
edit_profile: Edit profile edit_profile: Edit profile
preferences: Preferences preferences: Preferences
stream_entries: stream_entries:
click_to_show: Click to show
favourited: favourited a post by favourited: favourited a post by
is_now_following: is now following is_now_following: is now following
sensitive_content: Sensitive content
time:
formats:
default: "%b %d, %Y, %H:%M"
users: users:
invalid_email: The e-mail address is invalid invalid_email: The e-mail address is invalid
will_paginate: will_paginate:

Loading…
Cancel
Save