Skip to content

Commit

Permalink
#827 finishing touches to group members list (#866)
Browse files Browse the repository at this point in the history
* Refine groupMembers list

* change new tag to appear for 1 week

* include membersPending on GroupMembersAllModal

* Improve buttonSubmit when reducedMotion is enabled

* Improve groupmembers cypress tests

* Add "new" and "pending" pills to GroupMembersAllModal

* Pixel perfect .pills margins

* changes based on @taoeffect review

* Changes based on @taoeffect review

* Revert "isNewMember" logic to not use moment

* convert joinedData to ms

* remove moment from dep. #874 will remove its usage

* fix fn argument type number -> string

* changes based on @taoeffect review
  • Loading branch information
sandrina-p authored Mar 29, 2020
1 parent 3a72809 commit 586f44e
Show file tree
Hide file tree
Showing 14 changed files with 226 additions and 137 deletions.
5 changes: 3 additions & 2 deletions frontend/assets/style/components/_buttons.scss
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,12 @@ button,
color: transparent;
// Delay the spin for 500ms, in case load is ultra fast.
// That way the user won't even see the loading
transition: color 400ms 500ms;
// [1] !important so it works even when .js-reducedMotion
transition: color 400ms 500ms !important; // [1]

&::after {
opacity: 1;
transition: opacity 400ms 500ms;
transition: opacity 400ms 500ms !important; // [1]
animation: loadSpin 1.75s infinite linear;
}
}
Expand Down
25 changes: 8 additions & 17 deletions frontend/assets/style/components/_pills.scss
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
.pill {
font-weight: 600;
border-radius: 3px;
padding: 0.375rem $spacer-sm;
border-radius: $radius;
padding: 0.125rem 0.25rem;
line-height: 1;
margin: 0 $spacer-xs;
display: inline-block;
white-space: nowrap;
text-transform: uppercase;
font-size: $size_5;

&.is-small {
padding: ($spacer / 8) $spacer-xs;
text-transform: uppercase;
font-size: $size_5;
font-weight: 100;
}

&.has-background-dark {
background: $text_0;
color: $general_2;
&.is-neutral {
background-color: $general_0;
}

@each $name in $colors {
&.is-#{$name} {
@extend .pill;
@extend .has-background-#{$name};
@extend .has-text-#{$name};
background-color: var(--#{$name}_2);
color: var(--#{$name}_0);
}
}
}
22 changes: 18 additions & 4 deletions frontend/model/contracts/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ function initMonthlyPayments () {
}
}

function initGroupProfile (contractID: string) {
function initGroupProfile (contractID: string, joined: ?string) {
return {
contractID: contractID,
nonMonetaryContributions: []
nonMonetaryContributions: [],
joinedDate: joined
}
}

Expand Down Expand Up @@ -149,6 +150,19 @@ DefineContract({
groupMembersCount (state, getters) {
return getters.groupMembersByUsername.length
},
groupMembersPending (state, getters) {
const invites = getters.currentGroupState.invites
const pendingMembers = {}
for (const inviteId in invites) {
const invite = invites[inviteId]
if (invite.status === INVITE_STATUS.VALID && invite.creator !== INVITE_INITIAL_CREATOR) {
pendingMembers[invites[inviteId].invitee] = {
invitedBy: invites[inviteId].creator
}
}
}
return pendingMembers
},
groupShouldPropose (state, getters) {
return getters.groupMembersCount >= 3
},
Expand Down Expand Up @@ -199,7 +213,7 @@ DefineContract({
groupCreator: meta.username
},
profiles: {
[meta.username]: initGroupProfile(meta.identityContractID)
[meta.username]: initGroupProfile(meta.identityContractID, meta.createdDate)
},
userPaymentsByMonth: {
[currentMonthTimestamp()]: initMonthlyPayments()
Expand Down Expand Up @@ -436,7 +450,7 @@ DefineContract({
if (Object.keys(invite.responses).length === invite.quantity) {
invite.status = INVITE_STATUS.USED
}
Vue.set(state.profiles, meta.username, initGroupProfile(meta.identityContractID))
Vue.set(state.profiles, meta.username, initGroupProfile(meta.identityContractID, meta.createdDate))
// If we're triggered by handleEvent in state.js (and not latestContractState)
// then the asynchronous sideEffect function will get called next
// and we will subscribe to this new user's identity contract
Expand Down
6 changes: 6 additions & 0 deletions frontend/views/components/Avatar.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<template lang='pug'>
img.c-avatar(
v-if='imageURL'
:class='`is-${size}`'
:src='imageURL'
:alt='alt'
ref='img'
v-on='$listeners'
)
.c-avatar.is-empty(v-else :class='`is-${size}`')
</template>

<script>
Expand Down Expand Up @@ -117,6 +119,10 @@ export default {
padding-bottom: 100%;
}
&.is-empty {
background-color: $general_0;
}
&.is-xs { @include size(1.5rem); }
&.is-sm { @include size(2rem); }
&.is-md { @include size(2.5rem); }
Expand Down
2 changes: 1 addition & 1 deletion frontend/views/components/AvatarUser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default {
console.debug(`Looking for ${this.username} profile picture`)
const userContractId = await sbp('namespace/lookup', this.username)
if (!userContractId) {
console.error(`AvatarUser: ${this.username} doesn't exist!`)
console.warn(`AvatarUser: ${this.username} doesn't exist!`)
return
}
const state = await sbp('state/latestContractState', userContractId)
Expand Down
2 changes: 1 addition & 1 deletion frontend/views/components/menu/MenuContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default {
// Is that enought for every menu?
// Should we use mask transition instead?
pointer-events: initial;
max-height: 400px;
max-height: 25rem;
opacity: 1;
transition: max-height cubic-bezier(0.25, 0.46, 0.45, 0.94) 1s 100ms, opacity cubic-bezier(0.25, 0.46, 0.45, 0.94) 300ms 100ms;
}
Expand Down
102 changes: 56 additions & 46 deletions frontend/views/containers/dashboard/GroupMembers.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,20 @@
ul.c-group-list
li.c-group-member(
v-for='(member, username) in firstTenMembers'
:class='member.pending && "is-pending"'
:data-test='username'
:class='member.invitedBy && "is-pending"'
:key='username'
)
avatar-user(:username='username' size='sm')

.c-name.has-ellipsis(data-test='username')
| {{ username }}

i18n.pill.has-text-small.has-background-dark(
v-if='member.pending'
data-test='pending'
) pending

tooltip(
v-if='member.pending'
direction='bottom-end'
)
span.button.is-icon-small(
data-test='pendingTooltip'
)
i.icon-question-circle
template(slot='tooltip')
i18n(
tag='p'
:args='{ username }'
) We are waiting for {username} to join the group by using their unique invite link.

group-member-menu(v-else :username='username')
avatar(v-if='member.invitedBy' size='sm')
avatar-user(v-else :username='username' size='sm')

.c-name.has-ellipsis(data-test='username') {{ localizedName(username) }}

i18n.pill.is-neutral(v-if='member.invitedBy' data-test='pillPending') pending
i18n.pill.is-primary(v-else-if='isNewMember(username)' data-test='pillNew') new

group-members-tooltip-pending.c-menu(v-if='member.invitedBy' :username='username')
group-members-menu.c-menu(v-else :username='username')
i18n.link(
tag='button'
v-if='groupMembersCount > 10'
Expand All @@ -54,45 +40,69 @@
import { mapGetters } from 'vuex'
import { OPEN_MODAL } from '@utils/events.js'
import sbp from '~/shared/sbp.js'
import Avatar from '@components/Avatar.vue'
import AvatarUser from '@components/AvatarUser.vue'
import GroupMemberMenu from '@containers/dashboard/GroupMemberMenu.vue'
import Tooltip from '@components/Tooltip.vue'
import GroupMembersMenu from '@containers/dashboard/GroupMembersMenu.vue'
import GroupMembersTooltipPending from '@containers/dashboard/GroupMembersTooltipPending.vue'
import L from '@view-utils/translations.js'
export default {
name: 'GroupMembers',
components: {
Avatar,
AvatarUser,
GroupMemberMenu,
Tooltip
},
methods: {
invite () {
this.$router.push({ path: '/invite' })
},
openModal (modal, queries) {
sbp('okTurtles.events/emit', OPEN_MODAL, modal, queries)
}
GroupMembersMenu,
GroupMembersTooltipPending
},
computed: {
...mapGetters([
'groupProfiles',
'groupShouldPropose',
'groupMembersCount',
'ourUsername'
'ourUsername',
'userDisplayName',
'currentGroupState',
'groupMembersPending'
]),
weJoinedMs () {
return new Date(this.currentGroupState.profiles[this.ourUsername].joinedDate).getTime()
},
firstTenMembers () {
const profiles = this.groupProfiles
const usernames = Object.keys(profiles).slice(0, 10)
return usernames.reduce((acc, username) => {
const sliceIndex = 10 - Math.min(10, Object.keys(this.groupMembersPending).length) // avoid slicing too many members.
const usernames = Object.keys(profiles).slice(0, sliceIndex)
const members = usernames.reduce((acc, username) => {
// Prevent displaying users without a synced contract.
// It happens at the exact moment a user joins a group and both
// contracts (group + user) are still syncing
const profile = profiles[username]
if (profile) {
acc[username] = profile
acc[username] = { profile }
}
return acc
}, {})
// TODO - Sort members #869
return { ...this.groupMembersPending, ...members }
}
},
methods: {
invite () {
this.$router.push({ path: '/invite' })
},
openModal (modal, queries) {
sbp('okTurtles.events/emit', OPEN_MODAL, modal, queries)
},
isNewMember (username) {
if (username === this.ourUsername) { return false }
const memberJoinedMs = new Date(this.currentGroupState.profiles[username].joinedDate).getTime()
const joinedAfterUs = this.weJoinedMs < memberJoinedMs
return joinedAfterUs && Date.now() - memberJoinedMs < 604800000 // joined less than 1w (168h) ago.
},
localizedName (username) {
const name = this.userDisplayName(username)
return username === this.ourUsername ? L('{name} (you)', { name }) : name
}
}
}
Expand Down Expand Up @@ -134,16 +144,16 @@ export default {
.c-name {
margin-right: auto;
margin-left: $spacer-sm;
}
.is-pending & {
color: $text_1;
}
.c-menu {
margin-left: 0.5rem;
}
.c-actions-content.c-content {
top: calc(100% + #{$spacer-sm});
left: auto;
min-width: 214px;
min-width: 13rem;
}
</style>
Loading

0 comments on commit 586f44e

Please sign in to comment.