From c33931b613c7da4cc2c22ff8411c38556dc579cb Mon Sep 17 00:00:00 2001 From: Ian McCowan Date: Sun, 25 Feb 2018 18:31:28 -0800 Subject: Fix prev/next links on public profile page (#6497) * Fix prev/next links on public profile page * Don't make pagination urls if no available statuses * Fix empty check method * Put left chevron before prev page link * Add scope for pagination "starting at" a given id * Status pagination try 2: s/prev/older and s/next/newer "older" on left, "newer" on right Use new scope for "newer" link Extract magic 20 page size to constant Remove max_id from feed pagination as it's not respected * Reinstate max_id for accounts atom stream * normalize --- app/views/accounts/show.html.haml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'app/views') diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index accad5f78..21c585dab 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -39,6 +39,9 @@ = render partial: 'stream_entries/status', collection: @statuses, as: :status - - if @statuses.size == 20 + - if @newer_url || @older_url .pagination - = link_to safe_join([t('pagination.next'), fa_icon('chevron-right')], ' '), @next_url, class: 'next', rel: 'next' + - if @older_url + = link_to safe_join([fa_icon('chevron-left'), t('pagination.older')], ' '), @older_url, class: 'older', rel: 'older' + - if @newer_url + = link_to safe_join([t('pagination.newer'), fa_icon('chevron-right')], ' '), @newer_url, class: 'newer', rel: 'newer' -- cgit From 18513a978aecd36bf61a5cd7dba08f9f20729de9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 26 Feb 2018 16:18:41 +0100 Subject: Improve public account cards (#6559) - Add follow/unfollow/remote follow buttons - Format the bio properly - Always show username@domain, even for local accounts --- app/controllers/follower_accounts_controller.rb | 4 +- app/controllers/following_accounts_controller.rb | 4 +- app/javascript/styles/mastodon/accounts.scss | 67 ++++++++++++++---------- app/views/accounts/_follow_button.html.haml | 23 ++++++++ app/views/accounts/_grid_card.html.haml | 7 ++- app/views/accounts/_header.html.haml | 19 +------ 6 files changed, 73 insertions(+), 51 deletions(-) create mode 100644 app/views/accounts/_follow_button.html.haml (limited to 'app/views') diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 399e79665..2d2315034 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -7,7 +7,9 @@ class FollowerAccountsController < ApplicationController @follows = Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account) respond_to do |format| - format.html + format.html do + @relationships = AccountRelationshipsPresenter.new(@follows.map(&:account_id), current_user.account_id) if user_signed_in? + end format.json do render json: collection_presenter, diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 1e73d4bd4..169f9057d 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -7,7 +7,9 @@ class FollowingAccountsController < ApplicationController @follows = Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account) respond_to do |format| - format.html + format.html do + @relationships = AccountRelationshipsPresenter.new(@follows.map(&:target_account_id), current_user.account_id) if user_signed_in? + end format.json do render json: collection_presenter, diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index c812766a1..873963c90 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -97,32 +97,6 @@ } } - .controls { - position: absolute; - top: 15px; - left: 15px; - z-index: 2; - - .icon-button { - color: rgba($white, 0.8); - text-decoration: none; - font-size: 13px; - line-height: 13px; - font-weight: 500; - - .fa { - font-weight: 400; - margin-right: 5px; - } - - &:hover, - &:active, - &:focus { - color: $white; - } - } - } - .roles { margin-bottom: 30px; padding: 0 15px; @@ -226,6 +200,40 @@ } } +.card, +.account-grid-card { + .controls { + position: absolute; + top: 15px; + left: 15px; + z-index: 2; + + .icon-button { + color: rgba($white, 0.8); + text-decoration: none; + font-size: 13px; + line-height: 13px; + font-weight: 500; + + .fa { + font-weight: 400; + margin-right: 5px; + } + + &:hover, + &:active, + &:focus { + color: $white; + } + } + } +} + +.account-grid-card .controls { + left: auto; + right: 15px; +} + .pagination { padding: 30px 0; text-align: center; @@ -411,13 +419,14 @@ font-weight: 400; } - .note { + .account__header__content { padding: 10px 15px; padding-top: 15px; - box-sizing: border-box; color: lighten($ui-base-color, 26%); word-wrap: break-word; - min-height: 80px; + overflow: hidden; + text-overflow: ellipsis; + height: 5.5em; } } } diff --git a/app/views/accounts/_follow_button.html.haml b/app/views/accounts/_follow_button.html.haml new file mode 100644 index 000000000..e476e0aff --- /dev/null +++ b/app/views/accounts/_follow_button.html.haml @@ -0,0 +1,23 @@ +- relationships ||= nil + +- unless account.memorial? || account.moved? + - if user_signed_in? + - requested = relationships ? relationships.requested[account.id].present? : current_account.requested?(account) + - following = relationships ? relationships.following[account.id].present? : current_account.following?(account) + + - if user_signed_in? && current_account.id != account.id && !requested + .controls + - if following + = link_to account_unfollow_path(account), data: { method: :post }, class: 'icon-button' do + = fa_icon 'user-times' + = t('accounts.unfollow') + - else + = link_to account_follow_path(account), data: { method: :post }, class: 'icon-button' do + = fa_icon 'user-plus' + = t('accounts.follow') + - elsif !user_signed_in? + .controls + .remote-follow + = link_to account_remote_follow_path(account), class: 'icon-button' do + = fa_icon 'user-plus' + = t('accounts.remote_follow') diff --git a/app/views/accounts/_grid_card.html.haml b/app/views/accounts/_grid_card.html.haml index 305eb2c44..95acbd581 100644 --- a/app/views/accounts/_grid_card.html.haml +++ b/app/views/accounts/_grid_card.html.haml @@ -1,9 +1,12 @@ .account-grid-card .account-grid-card__header{ style: "background-image: url(#{account.header.url(:original)})" } + = render 'accounts/follow_button', account: account, relationships: @relationships .account-grid-card__avatar .avatar= image_tag account.avatar.url(:original) .name = link_to TagManager.instance.url_for(account) do %span.display_name.emojify= display_name(account) - %span.username @#{account.acct} - %p.note.emojify= truncate(strip_tags(account.note), length: 150) + %span.username + @#{account.local? ? account.local_username_and_domain : account.acct} + = fa_icon('lock') if account.locked? + .account__header__content.p-note.emojify= Formatter.instance.simplified_format(account) diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index d4081af64..b3c91b869 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -1,23 +1,6 @@ .card.h-card.p-author{ style: "background-image: url(#{account.header.url(:original)})" } .card__illustration - - unless account.memorial? || account.moved? - - if user_signed_in? && current_account.id != account.id && !current_account.requested?(account) - .controls - - if current_account.following?(account) - = link_to account_unfollow_path(account), data: { method: :post }, class: 'icon-button' do - = fa_icon 'user-times' - = t('accounts.unfollow') - - else - = link_to account_follow_path(account), data: { method: :post }, class: 'icon-button' do - = fa_icon 'user-plus' - = t('accounts.follow') - - elsif !user_signed_in? - .controls - .remote-follow - = link_to account_remote_follow_path(account), class: 'icon-button' do - = fa_icon 'user-plus' - = t('accounts.remote_follow') - + = render 'accounts/follow_button', account: account .avatar= image_tag account.avatar.url(:original), class: 'u-photo' .card__bio -- cgit From a40167cf4d51743965a90f1f7496b5a1e9e25f18 Mon Sep 17 00:00:00 2001 From: Lynx Kotoura Date: Tue, 27 Feb 2018 00:19:07 +0900 Subject: Better grid layout for the landing page (#6543) * Use grid layout for the landing page * Fix column settings Set the ratio explicitly * Improve information board --- app/javascript/styles/mastodon/about.scss | 249 +++++++++++++++++++----------- app/views/about/show.html.haml | 148 ++++++++++++------ 2 files changed, 262 insertions(+), 135 deletions(-) (limited to 'app/views') diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss index a95b75984..9417a924b 100644 --- a/app/javascript/styles/mastodon/about.scss +++ b/app/javascript/styles/mastodon/about.scss @@ -15,117 +15,172 @@ $small-breakpoint: 960px; } } -.show-xs, -.show-sm { - display: none; -} -.show-m { - display: block; -} -@media screen and (max-width: $small-breakpoint) { - .hide-sm { - display: none !important; - } +.landing-page { + .grid { + display: grid; + grid-gap: 10px; + grid-template-columns: 1fr 2fr; + grid-auto-columns: 25%; + grid-auto-rows: max-content; + + .column-0 { + display: none; + } - .show-sm { - display: block !important; - } -} + .column-1 { + grid-column: 1; + grid-row: 1; + } -@media screen and (max-width: $column-breakpoint) { - .hide-xs { - display: none !important; - } + .column-2 { + grid-column: 2; + grid-row: 1; + } + + .column-3 { + grid-column: 3; + grid-row: 1 / 3; + } - .show-xs { - display: block !important; + .column-4 { + grid-column: 1 / 3; + grid-row: 2; + } } -} -.row { - display: flex; - flex-wrap: wrap; - margin: 0 -5px; + @media screen and (max-width: $small-breakpoint) { - @for $i from 1 through 15 { - .column-#{$i} { - box-sizing: border-box; - min-height: 1px; - flex: 0 0 percentage($i / 15); - max-width: percentage($i / 15); - padding: 0 5px; + .grid { + grid-template-columns: 40% 60%; - @media screen and (max-width: $small-breakpoint) { - &-sm { - box-sizing: border-box; - min-height: 1px; - flex: 0 0 percentage($i / 15); - max-width: percentage($i / 15); - padding: 0 5px; + .column-0 { + display: none; + } - @media screen and (max-width: $column-breakpoint) { - max-width: 100%; - flex: 0 0 100%; - margin-bottom: 10px; + .column-1 { + grid-column: 1; + grid-row: 1; - &:last-child { - margin-bottom: 0; - } - } + &.non-preview .landing-page__forms { + height: 100%; } } - @media screen and (max-width: $column-breakpoint) { - max-width: 100%; - flex: 0 0 100%; - margin-bottom: 10px; + .column-2 { + grid-column: 2; + grid-row: 1 / 3; - &:last-child { - margin-bottom: 0; + &.non-preview { + grid-column: 2; + grid-row: 1; + } + } + + .column-3 { + grid-column: 1; + grid-row: 2 / 4; + } + + .column-4 { + grid-column: 2; + grid-row: 3; + + &.non-preview { + grid-column: 1 / 3; + grid-row: 2; } } } } -} -.column-flex { - display: flex; - flex-direction: column; -} + @media screen and (max-width: $column-breakpoint) { + .grid { + grid-template-columns: auto; -.separator-or { - position: relative; - margin: 40px 0; - text-align: center; + .column-0 { + display: block; + grid-column: 1; + grid-row: 1; + } - &::before { - content: ""; - display: block; - width: 100%; - height: 0; - border-bottom: 1px solid rgba($ui-base-lighter-color, .6); - position: absolute; - top: 50%; - left: 0; + .column-1 { + grid-column: 1; + grid-row: 3; + + .brand { + display: none; + } + } + + .column-2 { + grid-column: 1; + grid-row: 2; + + .landing-page__logo, + .landing-page__call-to-action { + display: none; + } + + &.non-preview { + grid-column: 1; + grid-row: 2; + } + } + + .column-3 { + grid-column: 1; + grid-row: 5; + } + + .column-4 { + grid-column: 1; + grid-row: 4; + + &.non-preview { + grid-column: 1; + grid-row: 4; + } + } + } } - span { - display: inline-block; - background: $ui-base-color; - font-size: 12px; - font-weight: 500; - color: $ui-primary-color; - text-transform: uppercase; + .column-flex { + display: flex; + flex-direction: column; + } + + .separator-or { position: relative; - z-index: 1; - padding: 0 8px; - cursor: default; + margin: 40px 0; + text-align: center; + + &::before { + content: ""; + display: block; + width: 100%; + height: 0; + border-bottom: 1px solid rgba($ui-base-lighter-color, .6); + position: absolute; + top: 50%; + left: 0; + } + + span { + display: inline-block; + background: $ui-base-color; + font-size: 12px; + font-weight: 500; + color: $ui-primary-color; + text-transform: uppercase; + position: relative; + z-index: 1; + padding: 0 8px; + cursor: default; + } } -} -.landing-page { p, li { font-family: 'mastodon-font-sans-serif', sans-serif; @@ -539,6 +594,7 @@ $small-breakpoint: 960px; img { position: static; + padding: 10px 0; } @media screen and (max-width: $small-breakpoint) { @@ -558,18 +614,33 @@ $small-breakpoint: 960px; } &__call-to-action { - margin-bottom: 10px; background: darken($ui-base-color, 4%); border-radius: 4px; padding: 25px 40px; overflow: hidden; .row { + display: flex; + flex-direction: row-reverse; + flex-wrap: wrap; + justify-content: space-between; align-items: center; } - .information-board__section { - padding: 0; + .row__information-board { + display: flex; + justify-content: flex-end; + align-items: flex-end; + + .information-board__section { + flex: 1 0 80px; + padding: 0 5px; + } + } + + .row__mascot { + flex: 1; + margin: 10px -50px 0 0; } } @@ -619,6 +690,8 @@ $small-breakpoint: 960px; &__short-description { .row { + display: flex; + flex-wrap: wrap; align-items: center; margin-bottom: 40px; } @@ -668,7 +741,6 @@ $small-breakpoint: 960px; height: 100%; @media screen and (max-width: $small-breakpoint) { - margin-bottom: 10px; height: auto; } @@ -717,6 +789,7 @@ $small-breakpoint: 960px; width: 100%; flex: 1 1 auto; overflow: hidden; + height: 100%; .column-header { color: inherit; diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml index fd1cda8b3..4f3cedacd 100644 --- a/app/views/about/show.html.haml +++ b/app/views/about/show.html.haml @@ -8,51 +8,100 @@ .landing-page.alternative .container - .row - .column-4.hide-sm.show-xs.show-m - .landing-page__forms - .brand - = link_to root_url do - = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' - - .hide-xs + .grid + .column-0 + .brand + = link_to root_url do + = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' + + - if Setting.timeline_preview + .column-1 + .landing-page__forms + .brand + = link_to root_url do + = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' + = render 'forms' - .column-7.column-9-sm - .landing-page__hero - = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title + - else + .column-1.non-preview + .landing-page__forms + .brand + = link_to root_url do + = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' - .landing-page__information - .landing-page__short-description + = render 'forms' + + - if Setting.timeline_preview + .column-2 + .landing-page__hero + = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title + + .landing-page__information + .landing-page__short-description + .row + .landing-page__logo + = image_tag asset_pack_path('logo_transparent.svg'), alt: 'Mastodon' + + %h1 + = @instance_presenter.site_title + %small!= t 'about.hosted_on', domain: content_tag(:span, site_hostname) + + %p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname) + + .landing-page__call-to-action .row - .landing-page__logo.hide-xs - = image_tag asset_pack_path('logo_transparent.svg'), alt: 'Mastodon' + .row__information-board + .information-board__section + %span= t 'about.user_count_before' + %strong= number_with_delimiter @instance_presenter.user_count + %span= t 'about.user_count_after' + .information-board__section + %span= t 'about.status_count_before' + %strong= number_with_delimiter @instance_presenter.status_count + %span= t 'about.status_count_after' + .row__mascot + .landing-page__mascot + = image_tag asset_pack_path('elephant_ui_plane.svg') - %h1 - = @instance_presenter.site_title - %small!= t 'about.hosted_on', domain: content_tag(:span, site_hostname) + - else + .column-2.non-preview + .landing-page__hero + = image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('preview.jpg'), alt: @instance_presenter.site_title - %p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname) + .landing-page__information + .landing-page__short-description + .row + .landing-page__logo + = image_tag asset_pack_path('logo_transparent.svg'), alt: 'Mastodon' - .show-xs - .landing-page__forms - = render 'forms' - .landing-page__call-to-action.hide-xs - .row - .column-5 - .landing-page__mascot - = image_tag asset_pack_path('elephant_ui_plane.svg') - .column-5 - .information-board__section - %span= t 'about.user_count_before' - %strong= number_with_delimiter @instance_presenter.user_count - %span= t 'about.user_count_after' - .column-5 - .information-board__section - %span= t 'about.status_count_before' - %strong= number_with_delimiter @instance_presenter.status_count - %span= t 'about.status_count_after' - .landing-page__information + %h1 + = @instance_presenter.site_title + %small!= t 'about.hosted_on', domain: content_tag(:span, site_hostname) + + %p= @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname) + + .landing-page__call-to-action + .row + .row__information-board + .information-board__section + %span= t 'about.user_count_before' + %strong= number_with_delimiter @instance_presenter.user_count + %span= t 'about.user_count_after' + .information-board__section + %span= t 'about.status_count_before' + %strong= number_with_delimiter @instance_presenter.status_count + %span= t 'about.status_count_after' + .row__mascot + .landing-page__mascot + = image_tag asset_pack_path('elephant_ui_plane.svg') + + - if Setting.timeline_preview + .column-3 + #mastodon-timeline{ data: { props: Oj.dump(default_props) } } + + - if Setting.timeline_preview + .column-4.landing-page__information .landing-page__features %h3= t 'about.what_is_mastodon' %p= t 'about.about_mastodon_html' @@ -67,13 +116,18 @@ = link_to t('about.source_code'), @instance_presenter.source_url = " (#{@instance_presenter.version_number})" - .column-4.column-6-sm.column-flex - .show-sm.hide-xs - .landing-page__forms - .brand - = link_to root_url do - = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' + - else + .column-4.non-preview.landing-page__information + .landing-page__features + %h3= t 'about.what_is_mastodon' + %p= t 'about.about_mastodon_html' - = render 'forms' - - if Setting.timeline_preview - #mastodon-timeline{ data: { props: Oj.dump(default_props) } } + = render 'features' + + .landing-page__features__action + = link_to t('about.learn_more'), 'https://joinmastodon.org/', class: 'button button-alternative' + + .landing-page__footer + %p + = link_to t('about.source_code'), @instance_presenter.source_url + = " (#{@instance_presenter.version_number})" -- cgit From 7150f2e9d3791720131dc77cd889acd0226d6acb Mon Sep 17 00:00:00 2001 From: Lynx Kotoura Date: Tue, 27 Feb 2018 01:43:45 +0900 Subject: Grid layout for tag pages (#6545) * Use grid layout for the landing page * Use grid layout for tag pages * Set 2 columns width as explicit percentage for tag pages --- app/javascript/styles/mastodon/about.scss | 113 ++++++++++-------------------- app/views/tags/_features.html.haml | 25 +++++++ app/views/tags/show.html.haml | 61 ++++++---------- 3 files changed, 84 insertions(+), 115 deletions(-) create mode 100644 app/views/tags/_features.html.haml (limited to 'app/views') diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss index 9417a924b..c2e819f51 100644 --- a/app/javascript/styles/mastodon/about.scss +++ b/app/javascript/styles/mastodon/about.scss @@ -1015,93 +1015,54 @@ $small-breakpoint: 960px; } &.tag-page { - .features { - padding: 30px 0; - - .container-alt { - max-width: 820px; - - #mastodon-timeline { - margin-right: 0; - border-top-right-radius: 0; - } - - .about-mastodon { - .about-hashtag { - background: darken($ui-base-color, 4%); - padding: 0 20px 20px 30px; - border-radius: 0 5px 5px 0; - - .brand { - padding-top: 20px; - margin-bottom: 20px; - - img { - height: 48px; - width: auto; - } - } - - p { - strong { - color: $ui-secondary-color; - font-weight: 700; - } - } + .grid { + @media screen and (min-width: $small-breakpoint) { + grid-template-columns: 33% 67%; + } - .cta { - margin: 0; + .column-2 { + grid-column: 2; + grid-row: 1; + } + } - .button { - margin-right: 4px; - } - } - } + .brand { + text-align: unset; + padding: 0; - .features-list { - margin-left: 30px; - margin-right: 10px; - } - } + img { + height: 48px; + width: auto; } } - @media screen and (max-width: 675px) { - .features { - padding: 10px 0; + .cta { + margin: 0; - .container-alt { - display: flex; - flex-direction: column; - - #mastodon-timeline { - order: 2; - flex: 0 0 auto; - height: 60vh; - margin-bottom: 20px; - border-top-right-radius: 4px; - } + .button { + margin-right: 4px; + } + } - .about-mastodon { - order: 1; - flex: 0 0 auto; - max-width: 100%; + @media screen and (max-width: $column-breakpoint) { + .grid { + .column-1 { + grid-column: 1; + grid-row: 2; + } - .about-hashtag { - background: unset; - padding: 0; - border-radius: 0; + .column-2 { + grid-column: 1; + grid-row: 1; + } + } - .cta { - margin: 20px 0; - } - } + .brand { + margin: 0; + } - .features-list { - display: none; - } - } - } + .landing-page__features { + display: none; } } } diff --git a/app/views/tags/_features.html.haml b/app/views/tags/_features.html.haml new file mode 100644 index 000000000..8fbc6b760 --- /dev/null +++ b/app/views/tags/_features.html.haml @@ -0,0 +1,25 @@ +.features-list + .features-list__row + .text + %h6= t 'about.features.real_conversation_title' + = t 'about.features.real_conversation_body' + .visual + = fa_icon 'fw comments' + .features-list__row + .text + %h6= t 'about.features.not_a_product_title' + = t 'about.features.not_a_product_body' + .visual + = fa_icon 'fw users' + .features-list__row + .text + %h6= t 'about.features.within_reach_title' + = t 'about.features.within_reach_body' + .visual + = fa_icon 'fw mobile' + .features-list__row + .text + %h6= t 'about.features.humane_approach_title' + = t 'about.features.humane_approach_body' + .visual + = fa_icon 'fw leaf' diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml index e4c16555d..f8cdc9952 100644 --- a/app/views/tags/show.html.haml +++ b/app/views/tags/show.html.haml @@ -6,48 +6,31 @@ = javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous' = render 'og' -.landing-page.tag-page +.landing-page.tag-page.alternative .features .container - #mastodon-timeline{ data: { props: Oj.dump(default_props.merge(hashtag: @tag.name)) } } + .grid + .column-1 + #mastodon-timeline{ data: { props: Oj.dump(default_props.merge(hashtag: @tag.name)) } } - .about-mastodon - .about-hashtag - .brand - = link_to root_url do - = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' + .column-2 + .about-mastodon + .about-hashtag.landing-page__information + .brand + = link_to root_url do + = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' - %p= t 'about.about_hashtag_html', hashtag: @tag.name + %p= t 'about.about_hashtag_html', hashtag: @tag.name - .cta - - if user_signed_in? - = link_to t('settings.back'), root_path, class: 'button button-secondary' - - else - = link_to t('auth.login'), new_user_session_path, class: 'button button-secondary' - = link_to t('about.learn_more'), about_path, class: 'button button-alternative' + .cta + - if user_signed_in? + = link_to t('settings.back'), root_path, class: 'button button-secondary' + - else + = link_to t('auth.login'), new_user_session_path, class: 'button button-secondary' + = link_to t('about.learn_more'), about_path, class: 'button button-alternative' - .features-list - .features-list__row - .text - %h6= t 'about.features.real_conversation_title' - = t 'about.features.real_conversation_body' - .visual - = fa_icon 'fw comments' - .features-list__row - .text - %h6= t 'about.features.not_a_product_title' - = t 'about.features.not_a_product_body' - .visual - = fa_icon 'fw users' - .features-list__row - .text - %h6= t 'about.features.within_reach_title' - = t 'about.features.within_reach_body' - .visual - = fa_icon 'fw mobile' - .features-list__row - .text - %h6= t 'about.features.humane_approach_title' - = t 'about.features.humane_approach_body' - .visual - = fa_icon 'fw leaf' + .landing-page__features.landing-page__information + %h3= t 'about.what_is_mastodon' + %p= t 'about.about_mastodon_html' + + = render 'features' -- cgit From 47bdb9b33b021c92bdfc6698914776eda13f6f77 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 28 Feb 2018 19:04:53 +0100 Subject: Fix #942: Seamless LDAP login (#6556) --- Gemfile | 1 + Gemfile.lock | 2 ++ app/controllers/application_controller.rb | 6 ++-- app/controllers/auth/sessions_controller.rb | 2 +- app/models/user.rb | 24 +++++++++++--- app/views/auth/passwords/edit.html.haml | 4 +-- app/views/auth/registrations/edit.html.haml | 4 +-- app/views/auth/sessions/new.html.haml | 2 +- config/application.rb | 1 + config/initializers/devise.rb | 34 ++++++++++++++++++++ config/locales/en.yml | 1 + lib/devise/ldap_authenticatable.rb | 49 +++++++++++++++++++++++++++++ 12 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 lib/devise/ldap_authenticatable.rb (limited to 'app/views') diff --git a/Gemfile b/Gemfile index fef7758cc..ed68534d4 100644 --- a/Gemfile +++ b/Gemfile @@ -33,6 +33,7 @@ gem 'devise', '~> 4.4' gem 'devise-two-factor', '~> 3.0' gem 'devise_pam_authenticatable2', '~> 8.0', install_if: -> { ENV['PAM_ENABLED'] == 'true' } +gem 'net-ldap', '~> 0.10', install_if: -> { ENV['LDAP_ENABLED'] == 'true' } gem 'omniauth-cas', '~> 1.1', install_if: -> { ENV['CAS_ENABLED'] == 'true' } gem 'omniauth-saml', '~> 1.8', install_if: -> { ENV['SAML_ENABLED'] == 'true' } gem 'omniauth', '~> 1.2' diff --git a/Gemfile.lock b/Gemfile.lock index 14f713604..8af55e432 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -316,6 +316,7 @@ GEM multi_json (1.12.2) multipart-post (2.0.0) necromancer (0.4.0) + net-ldap (0.16.1) net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (4.2.0) @@ -666,6 +667,7 @@ DEPENDENCIES memory_profiler microformats (~> 4.0) mime-types (~> 3.1) + net-ldap (~> 0.10) nokogiri (~> 1.8) nsa (~> 0.2) oj (~> 3.3) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 17c9dade8..6e5042617 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -14,7 +14,7 @@ class ApplicationController < ActionController::Base helper_method :current_session helper_method :current_theme helper_method :single_user_mode? - helper_method :use_pam? + helper_method :use_seamless_external_login? rescue_from ActionController::RoutingError, with: :not_found rescue_from ActiveRecord::RecordNotFound, with: :not_found @@ -76,8 +76,8 @@ class ApplicationController < ActionController::Base @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.exists? end - def use_pam? - Devise.pam_authentication + def use_seamless_external_login? + Devise.pam_authentication || Devise.ldap_authentication end def current_account diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 42a3cb62c..02447dde0 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -37,7 +37,7 @@ class Auth::SessionsController < Devise::SessionsController if session[:otp_user_id] User.find(session[:otp_user_id]) elsif user_params[:email] - if use_pam? && Devise.check_at_sign && user_params[:email].index('@').nil? + if use_seamless_external_login? && Devise.check_at_sign && user_params[:email].index('@').nil? User.joins(:account).find_by(accounts: { username: user_params[:email] }) else User.find_for_authentication(email: user_params[:email]) diff --git a/app/models/user.rb b/app/models/user.rb index b053292da..2995d6d54 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -52,7 +52,6 @@ class User < ApplicationRecord devise :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable - devise :pam_authenticatable if Devise.pam_authentication devise :omniauthable belongs_to :account, inverse_of: :user @@ -117,6 +116,12 @@ class User < ApplicationRecord acc.destroy! unless save end + def ldap_setup(_attributes) + self.confirmed_at = Time.now.utc + self.admin = false + save! + end + def confirmed? confirmed_at.present? end @@ -247,17 +252,17 @@ class User < ApplicationRecord end def password_required? - return false if Devise.pam_authentication + return false if Devise.pam_authentication || Devise.ldap_authentication super end def send_reset_password_instructions - return false if encrypted_password.blank? && Devise.pam_authentication + return false if encrypted_password.blank? && (Devise.pam_authentication || Devise.ldap_authentication) super end def reset_password!(new_password, new_password_confirmation) - return false if encrypted_password.blank? && Devise.pam_authentication + return false if encrypted_password.blank? && (Devise.pam_authentication || Devise.ldap_authentication) super end @@ -280,6 +285,17 @@ class User < ApplicationRecord end end + def self.ldap_get_user(attributes = {}) + resource = joins(:account).find_by(accounts: { username: attributes[Devise.ldap_uid.to_sym].first }) + + if resource.blank? + resource = new(email: attributes[:mail].first, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first }) + resource.ldap_setup(attributes) + end + + resource + end + def self.authenticate_with_pam(attributes = {}) return nil unless Devise.pam_authentication super diff --git a/app/views/auth/passwords/edit.html.haml b/app/views/auth/passwords/edit.html.haml index 703c821c0..12880c227 100644 --- a/app/views/auth/passwords/edit.html.haml +++ b/app/views/auth/passwords/edit.html.haml @@ -4,7 +4,7 @@ = simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| = render 'shared/error_messages', object: resource - - if !use_pam? || resource.encrypted_password.present? + - if !use_seamless_external_login?? || resource.encrypted_password.present? = f.input :reset_password_token, as: :hidden = f.input :password, autofocus: true, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' } @@ -13,6 +13,6 @@ .actions = f.button :button, t('auth.set_new_password'), type: :submit - else - = t('simple_form.labels.defaults.pam_account') + %p.hint= t('users.seamless_external_login') .form-footer= render 'auth/shared/links' diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml index ca18caa56..fac702b38 100644 --- a/app/views/auth/registrations/edit.html.haml +++ b/app/views/auth/registrations/edit.html.haml @@ -4,7 +4,7 @@ = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'auth_edit' }) do |f| = render 'shared/error_messages', object: resource - - if !use_pam? || resource.encrypted_password.present? + - if !use_seamless_external_login? || resource.encrypted_password.present? = f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } = f.input :password, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' } = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' } @@ -13,7 +13,7 @@ .actions = f.button :button, t('generic.save_changes'), type: :submit - else - = t('simple_form.labels.defaults.pam_account') + %p.hint= t('users.seamless_external_login') %hr/ diff --git a/app/views/auth/sessions/new.html.haml b/app/views/auth/sessions/new.html.haml index 1c3a0b6b4..0c9f9d5fe 100644 --- a/app/views/auth/sessions/new.html.haml +++ b/app/views/auth/sessions/new.html.haml @@ -5,7 +5,7 @@ = render partial: 'shared/og' = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| - - if use_pam? + - if use_seamless_external_login? = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.username_or_email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') } - else = f.input :email, autofocus: true, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') } diff --git a/config/application.rb b/config/application.rb index cd180782c..34b9dcf48 100644 --- a/config/application.rb +++ b/config/application.rb @@ -12,6 +12,7 @@ require_relative '../lib/paperclip/gif_transcoder' require_relative '../lib/paperclip/video_transcoder' require_relative '../lib/mastodon/snowflake' require_relative '../lib/mastodon/version' +require_relative '../lib/devise/ldap_authenticatable' Dotenv::Railtie.load diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index ba7ad9e6c..0dc202976 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -36,6 +36,26 @@ module Devise mattr_accessor :pam_controlled_service @@pam_controlled_service = nil + mattr_accessor :check_at_sign + @@check_at_sign = false + + mattr_accessor :ldap_authentication + @@ldap_authentication = false + mattr_accessor :ldap_host + @@ldap_host = nil + mattr_accessor :ldap_port + @@ldap_port = nil + mattr_accessor :ldap_method + @@ldap_method = nil + mattr_accessor :ldap_base + @@ldap_base = nil + mattr_accessor :ldap_uid + @@ldap_uid = nil + mattr_accessor :ldap_bind_dn + @@ldap_bind_dn = nil + mattr_accessor :ldap_password + @@ldap_password = nil + class Strategies::PamAuthenticatable def valid? super && ::Devise.pam_authentication @@ -45,6 +65,8 @@ end Devise.setup do |config| config.warden do |manager| + manager.default_strategies(scope: :user).unshift :ldap_authenticatable if Devise.ldap_authentication + manager.default_strategies(scope: :user).unshift :pam_authenticatable if Devise.pam_authentication manager.default_strategies(scope: :user).unshift :two_factor_authenticatable manager.default_strategies(scope: :user).unshift :two_factor_backupable end @@ -324,4 +346,16 @@ Devise.setup do |config| config.pam_default_service = ENV.fetch('PAM_DEFAULT_SERVICE') { 'rpam' } config.pam_controlled_service = ENV.fetch('PAM_CONTROLLED_SERVICE') { 'rpam' } end + + if ENV['LDAP_ENABLED'] == 'true' + config.ldap_authentication = true + config.check_at_sign = true + config.ldap_host = ENV.fetch('LDAP_HOST', 'localhost') + config.ldap_port = ENV.fetch('LDAP_PORT', 389).to_i + config.ldap_method = ENV.fetch('LDAP_METHOD', :simple_tls).to_sym + config.ldap_base = ENV.fetch('LDAP_BASE') + config.ldap_bind_dn = ENV.fetch('LDAP_BIND_DN') + config.ldap_password = ENV.fetch('LDAP_PASSWORD') + config.ldap_uid = ENV.fetch('LDAP_UID', 'cn') + end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 026426c84..797ec6ac1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -769,4 +769,5 @@ en: users: invalid_email: The e-mail address is invalid invalid_otp_token: Invalid two-factor code + seamless_external_login: You are logged in via an external service, so password and e-mail settings are not available. signed_in_as: 'Signed in as:' diff --git a/lib/devise/ldap_authenticatable.rb b/lib/devise/ldap_authenticatable.rb new file mode 100644 index 000000000..531abdbbe --- /dev/null +++ b/lib/devise/ldap_authenticatable.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +if ENV['LDAP_ENABLED'] == 'true' + require 'net/ldap' + require 'devise/strategies/authenticatable' + + module Devise + module Strategies + class LdapAuthenticatable < Authenticatable + def authenticate! + if params[:user] + ldap = Net::LDAP.new( + host: Devise.ldap_host, + port: Devise.ldap_port, + base: Devise.ldap_base, + encryption: { + method: Devise.ldap_method, + tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS, + }, + auth: { + method: :simple, + username: Devise.ldap_bind_dn, + password: Devise.ldap_password, + }, + connect_timeout: 10 + ) + + if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: "(#{Devise.ldap_uid}=#{email})", password: password)) + user = User.ldap_get_user(user_info.first) + success!(user) + else + return fail(:invalid_login) + end + end + end + + def email + params[:user][:email] + end + + def password + params[:user][:password] + end + end + end + end + + Warden::Strategies.add(:ldap_authenticatable, Devise::Strategies::LdapAuthenticatable) +end -- cgit From 473a69ab18c17551c99dc0806aa948b7edaf7cb0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 1 Mar 2018 02:48:08 +0100 Subject: Fix margin on top action button, fix width of counters on frontpage (#6573) --- app/javascript/styles/mastodon/about.scss | 7 ++----- app/javascript/styles/mastodon/forms.scss | 5 +++++ app/views/settings/preferences/show.html.haml | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) (limited to 'app/views') diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss index c2e819f51..9ce83aa9b 100644 --- a/app/javascript/styles/mastodon/about.scss +++ b/app/javascript/styles/mastodon/about.scss @@ -15,8 +15,6 @@ $small-breakpoint: 960px; } } - - .landing-page { .grid { display: grid; @@ -51,7 +49,6 @@ $small-breakpoint: 960px; } @media screen and (max-width: $small-breakpoint) { - .grid { grid-template-columns: 40% 60%; @@ -633,8 +630,8 @@ $small-breakpoint: 960px; align-items: flex-end; .information-board__section { - flex: 1 0 80px; - padding: 0 5px; + flex: 1 0 auto; + padding: 0 10px; } } diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index dec7d2284..2e38cda4e 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -278,6 +278,11 @@ code { .actions { margin-top: 30px; display: flex; + + &.actions--top { + margin-top: 0; + margin-bottom: 30px; + } } button, diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index 030719201..fd66e13fb 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -4,7 +4,7 @@ = simple_form_for current_user, url: settings_preferences_path, html: { method: :put } do |f| = render 'shared/error_messages', object: current_user - .actions + .actions.actions--top = f.button :button, t('generic.save_changes'), type: :submit %h4= t 'preferences.languages' -- cgit From 036dd98abb1fe6ae1d25ff0f3ecffe4dd9a79ea3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 2 Mar 2018 07:00:04 +0100 Subject: Responsively enforce 16:9 ratio on all media thumbnails in web UI (#6590) * Responsively enforce 16:9 ratio on all media thumbnails in web UI Also change video player behaviour to "contain" rather than "cover" videos that don't fit the ratio, unlike images and GIFs, it's expected that a video is shown fully. * Fix spacing issues and remove floor * Remove floor --- .../mastodon/components/media_gallery.js | 5 +++-- app/javascript/mastodon/components/status.js | 1 + .../features/status/components/detailed_status.js | 1 + app/javascript/mastodon/features/video/index.js | 25 +++++++++++++++++++--- app/javascript/styles/mastodon/components.scss | 2 +- .../stream_entries/_detailed_status.html.haml | 2 +- app/views/stream_entries/_simple_status.html.haml | 2 +- 7 files changed, 30 insertions(+), 8 deletions(-) (limited to 'app/views') diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 9e1bb77c2..3568a8440 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -283,8 +283,9 @@ export default class MediaGallery extends React.PureComponent { if (width) { style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']); } + } else if (width) { + style.height = width / (16/9); } else { - // crop the image style.height = height; } @@ -309,7 +310,7 @@ export default class MediaGallery extends React.PureComponent { if (this.isStandaloneEligible()) { children = ; } else { - children = media.take(4).map((attachment, i) => ); + children = media.take(4).map((attachment, i) => ); } } diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index c030510a0..c52cd5f09 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -184,6 +184,7 @@ export default class Status extends ImmutablePureComponent { src={video.get('url')} width={239} height={110} + inline sensitive={status.get('sensitive')} onOpenVideo={this.handleOpenVideo} /> diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index abdb9a3f6..d4f21fc32 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -57,6 +57,7 @@ export default class DetailedStatus extends ImmutablePureComponent { src={video.get('url')} width={300} height={150} + inline onOpenVideo={this.handleOpenVideo} sensitive={status.get('sensitive')} /> diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index c81a5cb5f..98ebcb6f9 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -97,6 +97,7 @@ export default class Video extends React.PureComponent { onOpenVideo: PropTypes.func, onCloseVideo: PropTypes.func, detailed: PropTypes.bool, + inline: PropTypes.bool, intl: PropTypes.object.isRequired, }; @@ -105,6 +106,7 @@ export default class Video extends React.PureComponent { duration: 0, paused: true, dragging: false, + containerWidth: false, fullscreen: false, hovered: false, muted: false, @@ -113,6 +115,12 @@ export default class Video extends React.PureComponent { setPlayerRef = c => { this.player = c; + + if (c) { + this.setState({ + containerWidth: c.offsetWidth, + }); + } } setVideoRef = c => { @@ -246,12 +254,23 @@ export default class Video extends React.PureComponent { } render () { - const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed } = this.props; - const { currentTime, duration, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; + const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed } = this.props; + const { containerWidth, currentTime, duration, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const progress = (currentTime / duration) * 100; + const playerStyle = {}; + + let { width, height } = this.props; + + if (inline && containerWidth) { + width = containerWidth; + height = containerWidth / (16/9); + + playerStyle.width = width; + playerStyle.height = height; + } return ( -
+