FEATURE: user and group cards on mobile (#7246)

This commit is contained in:
Joe 2019-03-25 20:37:17 +08:00 committed by Joffrey JAFFEUX
parent f072da1bfe
commit ec2123809f
19 changed files with 410 additions and 192 deletions

View File

@ -2,6 +2,7 @@ import { setting } from "discourse/lib/computed";
import { default as computed } from "ember-addons/ember-computed-decorators";
import CardContentsBase from "discourse/mixins/card-contents-base";
import CleansUp from "discourse/mixins/cleans-up";
import { groupPath } from "discourse/lib/url";
const maxMembersToDisplay = 10;
@ -40,7 +41,7 @@ export default Ember.Component.extend(CardContentsBase, CleansUp, {
@computed("group")
groupPath(group) {
return `${Discourse.BaseUri}/g/${group.name}`;
return groupPath(group.name);
},
_showCallback(username, $target) {
@ -88,6 +89,11 @@ export default Ember.Component.extend(CardContentsBase, CleansUp, {
showGroup(group) {
this.showGroup(group);
this._close();
},
showUser(user) {
this.showUser(user);
this._close();
}
}
});

View File

@ -205,8 +205,8 @@ export default Ember.Component.extend(
this._close();
},
showUser() {
this.showUser(this.get("user"));
showUser(username) {
this.showUser(username);
this._close();
},

View File

@ -1,3 +1,9 @@
import {
default as DiscourseURL,
userPath,
groupPath
} from "discourse/lib/url";
export default Ember.Controller.extend({
topic: Ember.inject.controller(),
application: Ember.inject.controller(),
@ -9,7 +15,11 @@ export default Ember.Controller.extend({
},
showUser(user) {
this.transitionToRoute("user", user);
DiscourseURL.routeTo(userPath(user.username_lower));
},
showGroup(group) {
DiscourseURL.routeTo(groupPath(group.name));
}
}
});

View File

@ -52,6 +52,10 @@ export function userPath(subPath) {
return Discourse.getURL(subPath ? `/u/${subPath}` : "/u");
}
export function groupPath(subPath) {
return Discourse.getURL(subPath ? `/g/${subPath}` : "/g");
}
let _jumpScheduled = false;
export function jumpToElement(elementId) {
if (_jumpScheduled || Ember.isEmpty(elementId)) {

View File

@ -26,8 +26,8 @@ export default Ember.Mixin.create({
username = Ember.Handlebars.Utils.escapeExpression(username.toString());
// Don't show on mobile or nested
if (this.site.mobileView || $target.parents(".card-content").length) {
// Don't show if nested
if ($target.parents(".card-content").length) {
this._close();
DiscourseURL.routeTo($target.attr("href"));
return false;
@ -97,10 +97,6 @@ export default Ember.Mixin.create({
}
this._close();
if (this.site.mobileView) {
return false;
}
}
return true;
@ -149,58 +145,67 @@ export default Ember.Mixin.create({
Ember.run.schedule("afterRender", () => {
if (target) {
let position = target.offset();
if (position) {
position.bottom = "unset";
if (!this.site.mobileView) {
let position = target.offset();
if (position) {
position.bottom = "unset";
if (rtl) {
// The site direction is rtl
position.right = $(window).width() - position.left + 10;
position.left = "auto";
let overage = $(window).width() - 50 - (position.right + width);
if (overage < 0) {
position.right += overage;
position.top += target.height() + 48;
verticalAdjustments += target.height() + 48;
}
} else {
// The site direction is ltr
position.left += target.width() + 10;
let overage = $(window).width() - 50 - (position.left + width);
if (overage < 0) {
position.left += overage;
position.top += target.height() + 48;
verticalAdjustments += target.height() + 48;
}
}
position.top -= $("#main-outlet").offset().top;
if (isFixed) {
position.top -= $("html").scrollTop();
//if content is fixed and will be cut off on the bottom, display it above...
if (
position.top + height + verticalAdjustments >
$(window).height() - 50
) {
position.bottom =
$(window).height() -
(target.offset().top - $("html").scrollTop());
if (verticalAdjustments > 0) {
position.bottom += 48;
if (rtl) {
// The site direction is rtl
position.right = $(window).width() - position.left + 10;
position.left = "auto";
let overage = $(window).width() - 50 - (position.right + width);
if (overage < 0) {
position.right += overage;
position.top += target.height() + 48;
verticalAdjustments += target.height() + 48;
}
} else {
// The site direction is ltr
position.left += target.width() + 10;
let overage = $(window).width() - 50 - (position.left + width);
if (overage < 0) {
position.left += overage;
position.top += target.height() + 48;
verticalAdjustments += target.height() + 48;
}
position.top = "unset";
}
}
const avatarOverflowSize = 44;
if (isDocked && position.top < avatarOverflowSize) {
position.top = avatarOverflowSize;
}
position.top -= $("#main-outlet").offset().top;
if (isFixed) {
position.top -= $("html").scrollTop();
//if content is fixed and will be cut off on the bottom, display it above...
if (
position.top + height + verticalAdjustments >
$(window).height() - 50
) {
position.bottom =
$(window).height() -
(target.offset().top - $("html").scrollTop());
if (verticalAdjustments > 0) {
position.bottom += 48;
}
position.top = "unset";
}
}
this.$().css(position);
const avatarOverflowSize = 44;
if (isDocked && position.top < avatarOverflowSize) {
position.top = avatarOverflowSize;
}
this.$().css(position);
}
}
if (this.site.mobileView) {
$(".card-cloak").removeClass("hidden");
let position = target.offset();
position.top = "10%"; // match modal behaviour
position.left = 0;
this.$().css(position);
}
this.$().toggleClass("docked-card", isDocked);
// After the card is shown, focus on the first link
@ -216,6 +221,9 @@ export default Ember.Mixin.create({
_hide() {
if (!this.get("visible")) {
this.$().css({ left: -9999, top: -9999 });
if (this.site.mobileView) {
$(".card-cloak").addClass("hidden");
}
}
},

View File

@ -3,7 +3,7 @@
<div class="card-row first-row">
<div class="group-card-avatar">
<a href="{{groupPath}}" {{action "showGroup" group}} class="card-huge-avatar">
<a href {{action "showGroup" group}} class="card-huge-avatar">
{{avatar-flair
flairURL=group.flair_url
flairBgColor=group.flair_bg_color
@ -14,7 +14,7 @@
<div class="names">
<span>
<h1 class="{{group.name}}">
<a href="{{groupPath}}" {{action "showGroup"}}>{{group.name}}</a>
<a href {{action "showGroup" group}} class='group-page-link'>{{group.name}}</a>
</h1>
{{#if group.full_name}}
<h2 class='full-name'>{{group.full_name}}</h2>
@ -23,18 +23,22 @@
{{/if}}
</span>
</div>
<div class="usercard-controls group-details-button">
{{group-membership-button
<ul class="usercard-controls group-details-button">
<li>
{{group-membership-button
model=group
showLogin=(route-action "showLogin")}}
</li>
{{#if group.messageable}}
{{d-button
<li>
{{d-button
action=(action "messageGroup")
class="btn-primary group-message-button inline"
icon="envelope"
label="groups.message"}}
</li>
{{/if}}
</div>
</ul>
</div>
{{#if group.bio_cooked}}
@ -67,10 +71,10 @@
<div class="card-row fourth-row">
<div class="members metadata">
{{#each group.members as |user|}}
<a href="{{user.path}}" {{action "showUser" user}} class="card-tiny-avatar">{{bound-avatar user "tiny"}}</a>
<a href {{action 'showUser' user}} class="card-tiny-avatar">{{bound-avatar user "tiny"}}</a>
{{/each}}
{{#if showMoreMembers}}
<a href="{{groupPath}}" {{action "showGroup" group}} class="more-members-link"><span
<a href {{action "showGroup" group}} class="more-members-link"><span
class="more-members-count">+{{moreMembersCount}}
{{i18n "more"}}</span></a>
{{/if}}
@ -79,4 +83,4 @@
{{/if}}
</div>
{{/if}}
{{/if}}

View File

@ -3,7 +3,7 @@
<div class="card-row first-row">
<div class="user-card-avatar">
<a href="{{user.path}}" {{action "showUser"}} class="card-huge-avatar">{{bound-avatar user "huge"}}</a>
<a href {{action "showUser" user}} class="card-huge-avatar">{{bound-avatar user "huge"}}</a>
{{#if user.primary_group_name}}
{{avatar-flair
flairURL=user.primary_group_flair_url
@ -16,10 +16,12 @@
<div class="names">
<span>
<h1 class="{{staff}} {{newUser}} {{if nameFirst "full-name" "username"}}">
<a href="{{user.path}}" {{action "showUser"}}>{{if nameFirst user.name (format-username username)}}
{{user-status user currentUser=currentUser}}</a>
<a href {{action "showUser" user}} class='user-profile-link'>
{{if nameFirst user.name (format-username username)}}
{{user-status user currentUser=currentUser}}
</a>
</h1>
{{plugin-outlet name="user-card-after-username" args=(hash user=user showUser=(action "showUser")) tagName=''}}
{{plugin-outlet name="user-card-after-username" args=(hash user=user showUser=(action "showUser" user)) tagName=''}}
{{#unless nameFirst}}
{{#if user.name}}
<h2 class='full-name'>{{user.name}}</h2>
@ -188,9 +190,11 @@
{{user-badge badge=ub.badge user=user}}
{{/each}}
{{#if showMoreBadges}}
{{#link-to 'user.badges' user class="user-badge more-user-badges"}}
{{i18n 'badges.more_badges' count=moreBadgesCount}}
{{/link-to}}
<span class='more-user-badges'>
{{#link-to 'user.badges' user}}
{{i18n 'badges.more_badges' count=moreBadgesCount}}
{{/link-to}}
</span>
{{/if}}
</div>
{{/if}}
@ -198,4 +202,4 @@
{{/if}}
</div>
{{/if}}
{{/if}}

View File

@ -1,47 +1,44 @@
<td>
{{~#unless expandPinned}}
<div class='pull-left'>
<a href="{{topic.lastPostUrl}}">{{avatar topic.lastPoster imageSize="large"}}</a>
<a href="{{topic.lastPostUrl}}" data-user-card="{{topic.last_poster_username}}">{{avatar topic.lastPoster imageSize="large"}}</a>
</div>
<div class='right'>
{{else}}
<div>
{{/unless~}}
<div class='main-link'>
{{~raw-plugin-outlet name="topic-list-before-status"}}
{{~raw "topic-status" topic=topic~}}
{{~topic-link topic~}}
{{~#if topic.featured_link~}}
{{~topic-featured-link topic~}}
{{~/if~}}
{{~#if topic.unseen~}}
&nbsp;<span class="badge-notification new-topic"></span>
{{~/if~}}
{{~#if expandPinned~}}
{{~raw "list/topic-excerpt" topic=topic~}}
{{~/if~}}
</div>
<div class='pull-right'>
{{raw "list/post-count-or-badges" topic=topic postBadgesEnabled=showTopicPostBadges}}
</div>
<div class="topic-item-stats clearfix">
{{#unless hideCategory}}
<div class='category'>
{{category-link topic.category}}
{{else}}
<div>
{{/unless~}}
<div class='main-link'>
{{~raw-plugin-outlet name="topic-list-before-status"}}
{{~raw "topic-status" topic=topic~}}
{{~topic-link topic~}}
{{~#if topic.featured_link~}}
{{~topic-featured-link topic~}}
{{~/if~}}
{{~#if topic.unseen~}}
&nbsp;<span class="badge-notification new-topic"></span>
{{~/if~}}
{{~#if expandPinned~}}
{{~raw "list/topic-excerpt" topic=topic~}}
{{~/if~}}
</div>
{{/unless}}
{{discourse-tags topic mode="list"}}
<div class="pull-right">
<div class='num activity last'>
<span class="age activity" title="{{topic.bumpedAtTitle}}"><a href="{{topic.lastPostUrl}}">{{format-date topic.bumpedAt format="tiny" noTitle="true"}}</a></span>
<div class='pull-right'>
{{raw "list/post-count-or-badges" topic=topic postBadgesEnabled=showTopicPostBadges}}
</div>
<div class="topic-item-stats clearfix">
{{#unless hideCategory}}
<div class='category'>
{{category-link topic.category}}
</div>
{{/unless}}
{{discourse-tags topic mode="list"}}
<div class="pull-right">
<div class='num activity last'>
<span class="age activity" title="{{topic.bumpedAtTitle}}"><a
href="{{topic.lastPostUrl}}">{{format-date topic.bumpedAt format="tiny" noTitle="true"}}</a>
</span>
</div>
</div>
<div class="clearfix"></div>
</div>
</div>
<div class="clearfix"></div>
</div>
</div>
</td>
</td>

View File

@ -1,3 +1,7 @@
{{#if site.mobileView}}
<div class="card-cloak hidden"></div>
{{/if}}
{{user-card-contents
currentPath=application.currentPath
topic=topic.model
@ -9,4 +13,5 @@
currentPath=application.currentPath
topic=topic.model
showUser=(action "showUser")
showGroup=(action "showGroup")
createNewMessageViaParams=(route-action "createNewMessageViaParams")}}

View File

@ -5,14 +5,11 @@ $avatar_margin: -50px; // negative margin makes avatars extend above cards
// shared styles for user and group cards
#user-card,
#group-card {
position: absolute;
width: $card_width;
z-index: z("usercard");
box-shadow: shadow("card");
color: $primary;
background: $secondary center center;
background-size: cover;
min-height: 175px;
transition: opacity 0.2s, transform 0.2s;
-webkit-transition: opacity 0.2s, -webkit-transform 0.2s;
opacity: 0;
@ -21,17 +18,8 @@ $avatar_margin: -50px; // negative margin makes avatars extend above cards
opacity: 1;
@include transform(scale(1));
}
&.fixed {
position: fixed;
z-index: z("composer", "content") + 1;
}
&.docked-card {
z-index: z("header") + 1;
}
.card-content {
padding: 12px;
padding: 10px;
background: rgba($secondary, 0.85);
margin-top: 80px;
&:after {
@ -44,74 +32,71 @@ $avatar_margin: -50px; // negative margin makes avatars extend above cards
}
}
&.no-bg {
min-height: 50px;
.card-content {
margin-top: 0;
}
}
.names {
flex: 1 1 auto;
margin-left: 0.75em;
span {
display: block;
.card-row:not(.first-row) {
margin-top: 0.5em;
}
// avatar - names - controls
.first-row {
.names {
padding-left: 1.25em;
span {
display: block;
}
}
.usercard-controls {
list-style-type: none;
margin: 0;
button {
width: 100%;
}
}
}
.btn {
margin-bottom: 5px;
}
h1 {
margin: 0;
line-height: $line-height-medium;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
a {
color: $primary;
}
.d-icon {
font-size: $font-down-1;
color: $primary;
}
}
h2 {
font-size: $font-up-1;
margin: 0;
font-weight: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
a {
color: $primary;
}
}
h3 {
display: inline;
margin-right: 0.5em;
font-size: $font-0;
font-weight: normal;
color: $primary;
.desc,
a {
color: $primary-high;
}
}
h1,
h2,
h3 {
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
h1,
h2 {
a {
color: $primary;
}
}
h2,
h3 {
font-weight: normal;
}
p {
margin: 0 0 5px 0;
}
.btn {
margin-bottom: 5px;
}
.usercard-controls {
list-style-type: none;
margin: 0;
button {
width: 100%;
min-width: 150px;
}
}
.card-row:not(.first-row) {
margin-top: 0.5em;
}
}
// styles for user cards
// styles for user cards only
#user-card {
// avatar - names - controls
.first-row {
@ -121,7 +106,6 @@ $avatar_margin: -50px; // negative margin makes avatars extend above cards
height: $avatar_width;
}
.user-card-avatar {
margin-right: 10px;
margin-top: $avatar_margin;
}
.new-user a {
@ -196,9 +180,9 @@ $avatar_margin: -50px; // negative margin makes avatars extend above cards
display: flex;
align-items: flex-start;
.user-badge {
display: flex;
white-space: nowrap;
margin: 0 0.5em 0 0;
overflow: hidden;
text-overflow: ellipsis;
background: $primary-very-low;
border: 1px solid $primary-low;
color: $primary;
@ -206,16 +190,17 @@ $avatar_margin: -50px; // negative margin makes avatars extend above cards
.badge-display-name {
overflow: hidden;
text-overflow: ellipsis;
max-width: 185px;
}
.more-user-badges {
overflow: hidden;
a {
@extend .user-badge;
}
}
}
}
}
// styles for group cards
// styles for group cards only
#group-card {
// avatar - names and controls
.first-row {
@ -223,19 +208,18 @@ $avatar_margin: -50px; // negative margin makes avatars extend above cards
.group-card-avatar {
margin-top: $avatar_margin;
}
.group-card-avatar {
.avatar-flair {
width: $avatar_width;
height: $avatar_width;
display: flex;
color: $primary;
.d-icon {
margin: auto;
font-size: $avatar_width / 1.5;
}
&.rounded {
border-radius: 50%;
}
.avatar-flair {
display: flex;
background-size: contain;
width: $avatar_width;
height: $avatar_width;
color: $primary;
.d-icon {
margin: auto;
font-size: $avatar_width / 1.5;
}
&.rounded {
border-radius: 50%;
}
}
}

View File

@ -9,7 +9,6 @@
@import "desktop/header";
@import "desktop/login";
@import "desktop/modal";
@import "desktop/user-card";
@import "desktop/category-list";
@import "desktop/latest-topic-list";
@import "desktop/topic-list";

View File

@ -0,0 +1,52 @@
// shared styles for user and group cards
#user-card,
#group-card {
position: absolute;
z-index: z("usercard");
&.fixed {
position: fixed;
z-index: z("composer", "content") + 1;
}
&.docked-card {
z-index: z("header") + 1;
}
// avatar - names - controls
.first-row {
.names {
flex: 1 1 auto;
}
.usercard-controls {
button {
min-width: 150px;
}
}
}
h1 {
.d-icon {
font-size: $font-down-1;
}
}
h2 {
font-size: $font-up-1;
}
h3 {
font-size: $font-0;
}
}
// styles for user cards only
#user-card {
// badges
.sixth-row {
.badge-section {
.user-badge {
display: block;
max-width: 185px;
margin: 0 0.5em 0 0;
}
.more-user-badges {
max-width: 125px;
}
}
}
}

View File

@ -0,0 +1,88 @@
$avatar_width: 120px;
// shared styles for user and group cards
#user-card,
#group-card {
position: fixed;
// mobile cards should always be on top of everything - 1102
z-index: z("mobile-composer") + 2;
max-width: 95vw;
margin: 0 2.5vw;
max-height: 90vh;
// avatar - names - controls
.first-row {
flex-wrap: wrap;
.names {
flex: 1 1 calc(100% - #{$avatar_width});
box-sizing: border-box;
}
.usercard-controls {
display: flex;
flex: 1;
margin-top: 1em;
button {
white-space: nowrap;
}
li {
flex: 1;
& + li {
margin-left: 0.5em;
}
}
}
}
h1 {
font-size: $font-up-3;
.d-icon {
font-size: $font-down-2;
}
}
h2 {
font-size: $font-0;
}
h3 {
font-size: $font-down-1;
}
}
// styles for user cards only
#user-card {
// badges
.sixth-row {
.badge-section {
flex-wrap: wrap;
> span {
display: flex;
flex: 0 1 50%;
max-width: 50%; // for text ellipsis
padding: 2px 0;
box-sizing: border-box;
&:nth-of-type(1),
&:nth-of-type(3) {
padding-right: 4px;
}
a {
width: 100%;
display: flex;
}
}
.user-badge {
display: flex;
margin: 0;
width: 100%;
}
}
}
}
// mobile card cloak
.card-cloak {
position: fixed;
top: 0;
left: 0;
z-index: z("mobile-composer") + 1; // 1101
height: 100vh;
width: 100vw;
background-color: rgba(black, 0.5);
animation: fadein 0.2s;
}

View File

@ -132,3 +132,17 @@ blockquote {
#simple-container {
width: 90%;
}
// this is used to provide visual feedback that indicates that the user / group
// cards are loading after the user clicks on an avatar. It's mostly for very slow
// connections. Users on good connections won't notice it.
[data-user-card]:focus {
.avatar,
+ .avatar-flair {
animation: wave 0.75s infinite;
animation-delay: 0.5s;
}
.avatar {
position: relative;
}
}

View File

@ -137,7 +137,6 @@
app/assets/stylesheets/desktop/topic-post.scss | 8 +-
app/assets/stylesheets/desktop/topic.scss | 5 +-
app/assets/stylesheets/desktop/upload.scss | 2 +-
app/assets/stylesheets/desktop/user-card.scss | 2 +-
app/assets/stylesheets/desktop/user.scss | 8 -
app/assets/stylesheets/mobile.scss | 1 +
app/assets/stylesheets/mobile/alert.scss | 2 +

View File

@ -1,11 +1,22 @@
import { acceptance } from "helpers/qunit-helpers";
import DiscourseURL from "discourse/lib/url";
acceptance("Group Card - Mobile", { mobileView: true });
QUnit.test("group card", async assert => {
await visit("/t/301/1");
assert.ok(invisible("#group-card"), "user card is invisible by default");
assert.ok(
invisible("#group-card"),
"mobile group card is invisible by default"
);
await click("a.mention-group:first");
assert.ok(visible(".group-details-container"), "group page should be shown");
assert.ok(visible("#group-card"), "mobile group card should appear");
sandbox.stub(DiscourseURL, "routeTo");
await click(".card-content a.group-page-link");
assert.ok(
DiscourseURL.routeTo.calledWith("/g/discourse"),
"it should navigate to the group page"
);
});

View File

@ -1,4 +1,5 @@
import { acceptance } from "helpers/qunit-helpers";
import DiscourseURL from "discourse/lib/url";
acceptance("Group Card");
@ -8,4 +9,11 @@ QUnit.test("group card", async assert => {
await click("a.mention-group:first");
assert.ok(visible("#group-card"), "card should appear");
sandbox.stub(DiscourseURL, "routeTo");
await click(".card-content a.group-page-link");
assert.ok(
DiscourseURL.routeTo.calledWith("/g/discourse"),
"it should navigate to the group page"
);
});

View File

@ -0,0 +1,22 @@
import { acceptance } from "helpers/qunit-helpers";
import DiscourseURL from "discourse/lib/url";
acceptance("User Card - Mobile", { mobileView: true });
QUnit.test("user card", async assert => {
await visit("/t/internationalization-localization/280");
assert.ok(
invisible("#user-card"),
"mobile user card is invisible by default"
);
await click("a[data-user-card=eviltrout]:first");
assert.ok(visible("#user-card"), "mobile user card should appear");
sandbox.stub(DiscourseURL, "routeTo");
await click(".card-content a.user-profile-link");
assert.ok(
DiscourseURL.routeTo.calledWith("/u/eviltrout"),
"it should navigate to the user profile"
);
});

View File

@ -4,13 +4,16 @@ import DiscourseURL from "discourse/lib/url";
acceptance("User Card");
QUnit.test("user card", async assert => {
await visit("/");
await visit("/t/internationalization-localization/280");
assert.ok(invisible("#user-card"), "user card is invisible by default");
await click("a[data-user-card=eviltrout]:first");
assert.ok(visible("#user-card"), "card should appear");
sandbox.stub(DiscourseURL, "routeTo");
await click(".card-content a.mention");
assert.ok(DiscourseURL.routeTo.calledWith("/u/eviltrout"));
await click(".card-content a.user-profile-link");
assert.ok(
DiscourseURL.routeTo.calledWith("/u/eviltrout"),
"it should navigate to the user profile"
);
});