Merge pull request #4157 from oswaldoacauan/userlist-2x-design

[HTML5 2.0] User-list Redesign
This commit is contained in:
Anton Georgiev 2017-08-08 10:56:56 -04:00 committed by GitHub
commit 34a3afc26b
34 changed files with 600 additions and 739 deletions

View File

@ -13,7 +13,7 @@
body {
font-family: 'Source Sans Pro', Arial, sans-serif;
font-size: 1rem; /* 16px */
background-color: #2A2D33;
background-color: #06172A;
}
a {
@ -42,7 +42,7 @@
}
</style>
</head>
<body style="background-color: #2A2D33">
<body style="background-color: #06172A">
<div id="app" role="document"></div>
<script src="/client/lib/sip.js"></script>
<script src="/client/lib/bbb_webrtc_bridge_sip.js"></script>

View File

@ -17,8 +17,19 @@ const parseMessage = (message) => {
return parsedMessage;
};
export default function addChat(meetingId, message) {
check(message, {
const chatType = (userName) => {
const CHAT_CONFIG = Meteor.settings.public.chat;
const typeByUser = {
[CHAT_CONFIG.system_username]: CHAT_CONFIG.type_system,
[CHAT_CONFIG.public_username]: CHAT_CONFIG.type_public,
};
return userName in typeByUser ? typeByUser[userName] : CHAT_CONFIG.type_private;
};
export default function addChat(meetingId, chat) {
check(chat, {
message: String,
fromColor: String,
toUserId: String,
@ -29,25 +40,22 @@ export default function addChat(meetingId, message) {
fromTimezoneOffset: Match.Maybe(Number),
});
const parsedMessage = message;
parsedMessage.message = parseMessage(message.message);
const fromUserId = message.fromUserId;
const toUserId = message.toUserId;
check(fromUserId, String);
check(toUserId, String);
const selector = Object.assign(
{ meetingId },
flat(message),
);
const selector = {
meetingId,
fromTime: chat.fromTime,
fromUserId: chat.fromUserId,
toUserId: chat.toUserId,
};
const modifier = {
$set: {
meetingId,
message: flat(parsedMessage, { safe: true }),
},
$set: Object.assign(
flat(chat, { safe: true }),
{
meetingId,
message: parseMessage(chat.message),
type: chatType(chat.toUsername),
},
),
};
const cb = (err, numChanged) => {
@ -56,8 +64,13 @@ export default function addChat(meetingId, message) {
}
const { insertedId } = numChanged;
const to = message.toUsername || 'PUBLIC';
return Logger.info(`Added chat id=${insertedId} from=${message.fromUsername} to=${to}`);
const to = chat.toUsername || 'PUBLIC';
if (insertedId) {
return Logger.info(`Added chat from=${chat.fromUsername} to=${to} time=${chat.fromTime}`);
}
return Logger.info(`Upserted chat from=${chat.fromUsername} to=${to} time=${chat.fromTime}`);
};
return Chat.upsert(selector, modifier, cb);

View File

@ -16,8 +16,8 @@ export default function clearUserSystemMessages(meetingId, userId) {
const selector = {
meetingId,
'message.fromUserId': CHAT_CONFIG.type_system,
'message.toUserId': userId,
fromUserId: CHAT_CONFIG.type_system,
toUserId: userId,
};
return Chat.remove(selector, Logger.info(`Removing system messages from: (${userId})`));

View File

@ -19,13 +19,13 @@ function chat(credentials) {
return Chat.find({
$or: [
{
'message.toUsername': PUBLIC_CHAT_USERNAME,
toUsername: PUBLIC_CHAT_USERNAME,
meetingId,
}, {
'message.fromUserId': requesterUserId,
fromUserId: requesterUserId,
meetingId,
}, {
'message.toUserId': requesterUserId,
toUserId: requesterUserId,
meetingId,
},
],

View File

@ -1,9 +1,9 @@
import { check } from 'meteor/check';
import addUser from '../modifiers/addUser';
export default function handleUserJoined({ body }, meetingId) {
const user = body;
check(user, Object);
return addUser(meetingId, user);

View File

@ -1,9 +1,22 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Users from '/imports/api/2.0/users';
import stringHash from 'string-hash';
import flat from 'flat';
import addVoiceUser from '/imports/api/2.0/voice-users/server/modifiers/addVoiceUser';
const COLOR_LIST = [
'#d32f2f', '#c62828', '#b71c1c', '#d81b60', '#c2185b', '#ad1457', '#880e4f',
'#8e24aa', '#7b1fa2', '#6a1b9a', '#4a148c', '#5e35b1', '#512da8', '#4527a0',
'#311b92', '#3949ab', '#303f9f', '#283593', '#1a237e', '#1976d2', '#1565c0',
'#0d47a1', '#0277bd', '#01579b', '#00838f', '#006064', '#00796b', '#00695c',
'#004d40', '#2e7d32', '#1b5e20', '#33691e', '#827717', '#bf360c', '#6d4c41',
'#5d4037', '#4e342e', '#3e2723', '#757575', '#616161', '#424242', '#212121',
'#546e7a', '#455a64', '#37474f', '#263238',
];
export default function addUser(meetingId, user) {
check(meetingId, String);
@ -37,30 +50,36 @@ export default function addUser(meetingId, user) {
// override moderator status of html5 client users, depending on a system flag
const dummyUser = Users.findOne(selector);
let userRole = user.role;
if (dummyUser &&
if (
dummyUser &&
dummyUser.clientType === 'HTML5' &&
user.role === ROLE_MODERATOR &&
!ALLOW_HTML5_MODERATOR) {
user.role = ROLE_VIEWER;
userRole === ROLE_MODERATOR &&
!ALLOW_HTML5_MODERATOR
) {
userRole = ROLE_VIEWER;
}
let userRoles = [];
userRoles.push(
const userRoles = [
'viewer',
user.presenter ? 'presenter' : false,
user.role === 'MODERATOR' ? 'moderator' : false,
);
userRole === ROLE_MODERATOR ? 'moderator' : false,
].filter(Boolean);
userRoles = userRoles.filter(Boolean);
/* While the akka-apps dont generate a color we just pick one
from a list based on the userId */
const color = COLOR_LIST[stringHash(user.intId) % COLOR_LIST.length];
const modifier = {
$set: Object.assign(
{ meetingId },
{ connectionStatus: 'online' },
{ roles: userRoles },
{ sortName: user.name.trim().toLowerCase() },
{
meetingId,
connectionStatus: 'online',
roles: userRoles,
sortName: user.name.trim().toLowerCase(),
color,
},
flat(user),
),
};

View File

@ -104,12 +104,12 @@ $bars-padding: $lg-padding-x - .45rem; // -.45 so user-list and chat title is al
}
@include mq($medium-up) {
flex: 0 20vw;
flex: 0 15vw;
order: 1;
}
@include mq($xlarge-up) {
flex-basis: 15vw;
flex-basis: 10vw;
}
}

View File

@ -84,8 +84,8 @@ class ChatDropdown extends Component {
const link = document.createElement('a');
const mimeType = 'text/plain';
link.setAttribute('download', 'chat.txt');
link.setAttribute('href', `data: ${mimeType} ;charset=utf-8,
link.setAttribute('download', `public-chat-${Date.now()}.txt`);
link.setAttribute('href', `data: ${mimeType} ;charset=utf-8,
${encodeURIComponent(ChatService.exportChat(ChatService.getPublicMessages()))}`);
link.click();
}}

View File

@ -1,8 +1,8 @@
@import "/imports/ui/stylesheets/variables/_all";
@import "/imports/ui/stylesheets/mixins/_scrollable";
.btn {
margin: 0;
flex: 0 0;
margin-left: $sm-padding-x / 2;
i{
color: black !important;

View File

@ -4,7 +4,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import styles from './styles';
import MessageForm from './message-form/component';
import MessageList from './message-list/component';
import ChatDropdownContainer from './chat-dropdown/component';
import ChatDropdown from './chat-dropdown/component';
import Icon from '../icon/component';
const ELEMENT_ID = 'chat-messages';
@ -39,7 +39,6 @@ const Chat = (props) => {
return (
<div className={styles.chat}>
<header className={styles.header}>
<div className={styles.title}>
<Link
@ -50,26 +49,19 @@ const Chat = (props) => {
<Icon iconName="left_arrow" /> {title}
</Link>
</div>
<div className={styles.closeIcon}>
{
((chatID === 'public') ?
null :
<Link
to="/users"
role="button"
aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}
>
<Icon iconName="close" onClick={() => actions.handleClosePrivateChat(chatID)} />
</Link>)
}{
((chatID === 'public') ?
<ChatDropdownContainer />
:
null)
}
</div>
{
chatID !== 'public' ?
<Link
to="/users"
role="button"
className={styles.closeIcon}
aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}
>
<Icon iconName="close" onClick={() => actions.handleClosePrivateChat(chatID)} />
</Link> :
<ChatDropdown />
}
</header>
<MessageList
chatId={chatID}
messages={messages}

View File

@ -39,7 +39,7 @@ export default injectIntl(createContainer(({ params, intl }) => {
let chatName = title;
if (chatID === PUBLIC_CHAT_KEY) {
messages = ChatService.reducedPublicMessages((ChatService.getPublicMessages()));
messages = ChatService.reduceAndMapMessages((ChatService.getPublicMessages()));
} else {
messages = ChatService.getPrivateMessages(chatID);
}

View File

@ -51,6 +51,16 @@ class MessageForm extends Component {
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount() {
this.textarea.focus();
}
componentDidUpdate(prevProps) {
if (prevProps.chatName !== this.props.chatName) {
this.textarea.focus();
}
}
handleMessageKeyDown(e) {
// TODO Prevent send message pressing enter on mobile and/or virtual keyboard
if (e.keyCode === 13 && !e.shiftKey) {
@ -132,6 +142,7 @@ class MessageForm extends Component {
<TextareaAutosize
className={styles.input}
id="message-input"
innerRef={ref => this.textarea = ref}
placeholder={intl.formatMessage(messages.inputPlaceholder, { 0: chatName })}
aria-controls={this.props.chatAreaId}
aria-label={intl.formatMessage(messages.inputLabel, { 0: chatTitle })}
@ -146,12 +157,14 @@ class MessageForm extends Component {
onKeyDown={this.handleMessageKeyDown}
/>
<Button
hideLabel
circle
className={styles.sendButton}
aria-label={intl.formatMessage(messages.submitLabel)}
type="submit"
disabled={disabled}
label={intl.formatMessage(messages.submitLabel)}
hideLabel
color="primary"
icon="send"
onClick={() => null}
/>

View File

@ -51,7 +51,7 @@
-webkit-appearance: none;
box-shadow: none;
outline: 0;
padding: $sm-padding-y*2 $sm-padding-x;
padding: $sm-padding-y * 2.5 $sm-padding-x * 1.25;
resize: none;
transition: none;
border-radius: $border-radius;
@ -68,14 +68,7 @@
}
.sendButton {
@extend .input;
border-left: 0px;
min-width: 1rem;
max-width: 2.5rem;
> [class*=" icon-bbb-"] {
color: $color-gray-light;
}
margin-left: $sm-padding-x;
}
.info {

View File

@ -88,54 +88,6 @@ export default class MessageListItem extends Component {
return !nextState.preventRender && nextState.pendingChanges;
}
render() {
const {
user,
messages,
time,
} = this.props;
const dateTime = new Date(time);
if (!user) {
return this.renderSystemMessage();
}
return (
<div className={styles.item}>
<div className={styles.wrapper} ref={(ref) => { this.item = ref; }}>
<div className={styles.avatar}>
<UserAvatar user={user} />
</div>
<div className={styles.content}>
<div className={styles.meta}>
<div className={!user.isOnline ? styles.name : styles.logout}>
<span>{user.name}</span>
{user.isOnline ? null : <span className={styles.offline}>(offline)</span>}
</div>
<time className={styles.time} dateTime={dateTime}>
<FormattedTime value={dateTime} />
</time>
</div>
<div className={styles.messages}>
{messages.map((message, i) => (
<Message
className={styles.message}
key={message.id}
text={message.text}
time={message.time}
chatAreaId={this.props.chatAreaId}
lastReadMessageTime={this.props.lastReadMessageTime}
handleReadMessage={this.props.handleReadMessage}
/>
))}
</div>
</div>
</div>
</div>
);
}
renderSystemMessage() {
const {
messages,
@ -160,6 +112,60 @@ export default class MessageListItem extends Component {
</div>
);
}
render() {
const {
user,
messages,
time,
} = this.props;
const dateTime = new Date(time);
if (!user) {
return this.renderSystemMessage();
}
return (
<div className={styles.item}>
<div className={styles.wrapper} ref={(ref) => { this.item = ref; }}>
<div className={styles.avatarWrapper}>
<UserAvatar
className={styles.avatar}
color={user.color}
moderator={user.isModerator}
>
{user.name.toLowerCase().slice(0, 2)}
</UserAvatar>
</div>
<div className={styles.content}>
<div className={styles.meta}>
<div className={user.isOnline ? styles.name : styles.logout}>
<span>{user.name}</span>
{user.isOnline ? null : <span className={styles.offline}>(offline)</span>}
</div>
<time className={styles.time} dateTime={dateTime}>
<FormattedTime value={dateTime} />
</time>
</div>
<div className={styles.messages}>
{messages.map(message => (
<Message
className={styles.message}
key={message.id}
text={message.text}
time={message.time}
chatAreaId={this.props.chatAreaId}
lastReadMessageTime={this.props.lastReadMessageTime}
handleReadMessage={this.props.handleReadMessage}
/>
))}
</div>
</div>
</div>
</div>
);
}
}
MessageListItem.propTypes = propTypes;

View File

@ -16,17 +16,25 @@
}
.systemMessage {
.item + &,
& + .item {
margin-bottom: $line-height-computed * 1.5;
}
.message {
color: $color-heading;
}
}
.avatarWrapper {
flex-basis: 1.65rem;
flex-shrink: 0;
flex-grow: 0;
margin-right: $line-height-computed / 2;
}
.avatar {
flex-basis: 2.2rem;
flex-shrink: 0;
flex-grow: 0;
font-size: .85rem;
}
.content {
@ -34,13 +42,13 @@
display: flex;
flex-flow: column;
overflow: hidden;
margin-left: $line-height-computed / 2;
}
.meta {
display: flex;
flex: 1;
flex-flow: row;
line-height: 1;
& + .message {
margin-top: 0;
@ -51,7 +59,7 @@
display: flex;
min-width: 0;
font-weight: 600;
color: $color-gray;
color: $color-heading;
> span {
@extend %text-elipsis;
@ -98,13 +106,13 @@
.messages {
> .message:first-child {
margin-top: 0;
margin-top: $line-height-computed / 4;
}
}
.message {
flex: 1;
margin-top: $line-height-computed / 2;
margin-top: $line-height-computed / 3;
margin-bottom: 0;
color: $color-text;
word-wrap: break-word;

View File

@ -1,6 +1,8 @@
@import "/imports/ui/stylesheets/variables/_all";
@import "/imports/ui/stylesheets/mixins/_scrollable";
$padding: $md-padding-x;
.messageListWrapper {
display: flex;
flex-flow: column;
@ -9,10 +11,10 @@
position: relative;
overflow-x: hidden;
overflow-y: auto;
padding-right: 12px;
padding-bottom: 12px;
margin-right: -0.75rem;
margin-bottom: -0.75rem;
padding-right: $padding;
padding-bottom: $padding;
margin-right: -$padding;
margin-bottom: -$padding;
}
.messageList {
@ -20,10 +22,10 @@
flex-flow: column;
flex-grow: 1;
flex-shrink: 1;
margin-right: -$sm-padding-x;
padding-right: $sm-padding-x;
margin-right: -$padding;
padding-right: $padding;
padding-top: 0;
padding-bottom: $sm-padding-x;
padding-bottom: $padding;
}
.unreadButton {

View File

@ -11,6 +11,7 @@ const CHAT_CONFIG = Meteor.settings.public.chat;
const GROUPING_MESSAGES_WINDOW = CHAT_CONFIG.grouping_messages_window;
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
const PUBLIC_CHAT_TYPE = CHAT_CONFIG.type_public;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
const PUBLIC_CHAT_USERID = CHAT_CONFIG.public_userid;
@ -21,8 +22,8 @@ const ScrollCollection = new Mongo.Collection(null);
// session for closed chat list
const CLOSED_CHAT_LIST_KEY = 'closedChatList';
const getUser = (userID) => {
const user = Users.findOne({ userId: userID });
const getUser = (userId) => {
const user = Users.findOne({ userId });
if (!user) {
return null;
@ -31,18 +32,16 @@ const getUser = (userID) => {
return mapUser(user);
};
const mapMessage = (messagePayload) => {
const { message } = messagePayload;
const mapMessage = (message) => {
const mappedMessage = {
id: messagePayload._id,
content: messagePayload.content,
id: message._id,
content: message.content,
time: message.fromTime, // + message.from_tz_offset,
sender: null,
};
if (message.chat_type !== SYSTEM_CHAT_TYPE) {
mappedMessage.sender = getUser(message.fromUserId, message.fromUsername);
if (message.type !== SYSTEM_CHAT_TYPE) {
mappedMessage.sender = getUser(message.fromUserId);
}
return mappedMessage;
@ -50,51 +49,55 @@ const mapMessage = (messagePayload) => {
const reduceMessages = (previous, current) => {
const lastMessage = previous[previous.length - 1];
const currentPayload = current.message;
const currentMessage = current;
const reducedMessages = current;
reducedMessages.content = [];
reducedMessages.content.push({
currentMessage.content = [{
id: current._id,
text: currentPayload.message,
time: currentPayload.fromTime,
});
text: current.message,
time: current.fromTime,
}];
if (!lastMessage || !reducedMessages.message.chatType === SYSTEM_CHAT_TYPE) {
return previous.concat(reducedMessages);
if (!lastMessage || !currentMessage.type === SYSTEM_CHAT_TYPE) {
return previous.concat(currentMessage);
}
const lastPayload = lastMessage.message;
// Check if the last message is from the same user and time discrepancy
// between the two messages exceeds window and then group current message
// with the last one
if (lastPayload.fromUserId === currentPayload.fromUserId
&& (currentPayload.fromTime - lastPayload.fromTime) <= GROUPING_MESSAGES_WINDOW) {
lastMessage.content.push(reducedMessages.content.pop());
if (lastMessage.fromUserId === currentMessage.fromUserId
&& (currentMessage.fromTime - lastMessage.fromTime) <= GROUPING_MESSAGES_WINDOW) {
lastMessage.content.push(currentMessage.content.pop());
return previous;
}
return previous.concat(reducedMessages);
return previous.concat(currentMessage);
};
const reducedPublicMessages = publicMessages =>
(publicMessages.reduce(reduceMessages, []).map(mapMessage));
const reduceAndMapMessages = messages =>
(messages.reduce(reduceMessages, []).map(mapMessage));
const getPublicMessages = () => {
const publicMessages = Chats.find({
type: { $in: [PUBLIC_CHAT_TYPE, SYSTEM_CHAT_TYPE] },
}, {
sort: ['fromTime'],
}).fetch();
return publicMessages;
};
const getPrivateMessages = (userID) => {
const messages = Chats.find({
'message.toUsername': { $ne: PUBLIC_CHAT_USERNAME },
toUsername: { $ne: PUBLIC_CHAT_USERNAME },
$or: [
{ 'message.toUserId': userID },
{ 'message.fromUserId': userID },
{ toUserId: userID },
{ fromUserId: userID },
],
}, {
sort: ['message.fromTime'],
sort: ['fromTime'],
}).fetch();
return messages.reduce(reduceMessages, []).map(mapMessage);
return reduceAndMapMessages(messages);
};
const isChatLocked = (receiverID) => {
@ -199,34 +202,22 @@ const htmlDecode = (input) => {
return e.childNodes[0].nodeValue;
};
const formatTime = time => (time <= 9 ? `0${time}` : time);
// Export the chat as [Hour:Min] user : message
// Export the chat as [Hour:Min] user: message
const exportChat = messageList => (
messageList.map(({ message }) => {
messageList.map((message) => {
const date = new Date(message.fromTime);
const hour = formatTime(date.getHours());
const min = formatTime(date.getMinutes());
const hourMin = `${hour}:${min}`;
if (message.fromUserId === SYSTEM_CHAT_TYPE) {
return `[${hourMin}] ${message.message}`;
const hour = date.getHours().toString().padStart(2, 0);
const min = date.getMinutes().toString().padStart(2, 0);
const hourMin = `[${hour}:${min}]`;
if (message.type === SYSTEM_CHAT_TYPE) {
return `${hourMin} ${message.message}`;
}
return `[${hourMin}] ${message.fromUsername}: ${htmlDecode(message.message)}`;
return `${hourMin} ${message.fromUsername}: ${htmlDecode(message.message)}`;
}).join('\n')
);
const getPublicMessages = () => {
const publicMessages = Chats.find({
'message.toUsername': { $in: [PUBLIC_CHAT_USERNAME, SYSTEM_CHAT_TYPE] },
}, {
sort: ['message.fromTime'],
}).fetch();
return publicMessages;
};
export default {
reducedPublicMessages,
reduceAndMapMessages,
getPublicMessages,
getPrivateMessages,
getUser,

View File

@ -2,7 +2,7 @@
.chat {
background-color: #fff;
padding: $sm-padding-x;
padding: $md-padding-x;
display: flex;
flex-grow: 1;
flex-direction: column;
@ -11,33 +11,29 @@
}
.header {
margin-top: $lg-padding-x - $sm-padding-x;
margin-bottom: $lg-padding-x;
margin-bottom: $md-padding-x;
display: flex;
flex-direction: row;
align-items: center;
align-items: left;
flex-shrink: 0;
a {
text-decoration: none;
display: block;
}
[class^="icon-bbb-"],
[class*=" icon-bbb-"] {
font-size: 85%;
}
}
.title {
@extend %text-elipsis;
width: 90%;
> [class^="icon-bbb-"],
> [class*=" icon-bbb-"] {
font-size: 85%;
}
flex: 1;
}
.closeIcon {
position: relative;
}
[class='icon-bbb-left_arrow'],
[class='icon-bbb-close']{
padding-bottom: 5px;
flex: 0 0;
margin-left: $sm-padding-x / 2;
}

View File

@ -1,108 +0,0 @@
// This is the code to generate the colors for the user avatar when no image is provided
const stringToPastelColour = (str) => {
str = str.trim().toLowerCase();
const baseRed = 128;
const baseGreen = 128;
const baseBlue = 128;
let seed = 0;
for (let i = 0; i < str.length; seed = str.charCodeAt(i++) + ((seed << 5) - seed));
const a = Math.abs((Math.sin(seed++) * 10000)) % 256;
const b = Math.abs((Math.sin(seed++) * 10000)) % 256;
const c = Math.abs((Math.sin(seed++) * 10000)) % 256;
// build colour
const red = Math.round((a + baseRed) / 2);
const green = Math.round((b + baseGreen) / 2);
const blue = Math.round((c + baseBlue) / 2);
return {
r: red,
g: green,
b: blue,
};
};
// https://www.w3.org/TR/WCAG20/#relativeluminancedef
// http://entropymine.com/imageworsener/srgbformula/
const relativeLuminance = (rgb) => {
const tmp = {};
Object.keys(rgb).forEach((i) => {
const c = rgb[i] / 255;
if (c <= 0.03928) {
tmp[i] = c / 12.92;
} else {
tmp[i] = Math.pow(((c + 0.055) / 1.055), 2.4);
}
});
return (0.2126 * tmp.r + 0.7152 * tmp.g + 0.0722 * tmp.b);
};
/**
* Calculate contrast ratio acording to WCAG 2.0 formula
* Will return a value between 1 (no contrast) and 21 (max contrast)
* @link http://www.w3.org/TR/WCAG20/#contrast-ratiodef
*/
const contrastRatio = (a, b) => {
let c;
a = relativeLuminance(a);
b = relativeLuminance(b);
// Arrange so a is lightest
if (a < b) {
c = a;
a = b;
b = c;
}
return (a + 0.05) / (b + 0.05);
};
const shadeColor = (rgb, amt) => {
let r = rgb.r + amt;
if (r > 255) r = 255;
else if (r < 0) r = 0;
let b = rgb.b + amt;
if (b > 255) b = 255;
else if (b < 0) b = 0;
let g = rgb.g + amt;
if (g > 255) g = 255;
else if (g < 0) g = 0;
return {
r,
g,
b,
};
};
const addShadeIfNoContrast = (rgb) => {
const base = {
r: 255,
g: 255,
b: 255,
}; // white
const cr = contrastRatio(base, rgb);
if (cr > 4.5) {
return rgb;
}
return addShadeIfNoContrast(shadeColor(rgb, -25));
};
const getColor = (str) => {
let rgb = stringToPastelColour(str);
rgb = addShadeIfNoContrast(rgb);
return `rgb(${rgb.r},${rgb.g},${rgb.b})`;
};
export default getColor;

View File

@ -1,120 +1,63 @@
import React, { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import Icon from '/imports/ui/components/icon/component';
import styles from './styles.scss';
import cx from 'classnames';
import generateColor from './color-generator';
import styles from './styles.scss';
const propTypes = {
user: PropTypes.shape({
name: PropTypes.string.isRequired,
isPresenter: PropTypes.bool.isRequired,
isVoiceUser: PropTypes.bool.isRequired,
isModerator: PropTypes.bool.isRequired,
isOnline: PropTypes.bool.isRequired,
image: PropTypes.string,
}).isRequired,
children: PropTypes.node.isRequired,
moderator: PropTypes.bool.isRequired,
presenter: PropTypes.bool.isRequired,
talking: PropTypes.bool.isRequired,
muted: PropTypes.bool.isRequired,
listenOnly: PropTypes.bool.isRequired,
voice: PropTypes.bool.isRequired,
color: PropTypes.string,
className: PropTypes.string,
};
const defaultProps = {
moderator: false,
presenter: false,
talking: false,
muted: false,
listenOnly: false,
voice: false,
color: '#000',
};
export default class UserAvatar extends Component {
render() {
const {
user,
} = this.props;
const avatarColor = user.isOnline ? generateColor(user.name) : '#fff';
const avatarStyles = {
backgroundColor: avatarColor,
boxShadow: user.isTalking ? `0 0 .5rem ${avatarColor}` : 'none',
};
return (
<div
className={user.isOnline ? styles.userAvatar : styles.userLogout}
style={avatarStyles} aria-hidden="true"
>
<div>
{this.renderAvatarContent()}
</div>
{this.renderUserStatus()}
{this.renderUserMediaStatus()}
</div>
);
}
renderAvatarContent() {
const user = this.props.user;
let content = <span aria-hidden="true" className={styles.avatarText}>{user.name.slice(0, 2)}</span>;
if (user.emoji.status !== 'none') {
let iconEmoji;
switch (user.emoji.status) {
case 'thumbsUp':
iconEmoji = 'thumbs_up';
break;
case 'thumbsDown':
iconEmoji = 'thumbs_down';
break;
case 'raiseHand':
iconEmoji = 'hand';
break;
case 'away':
iconEmoji = 'time';
break;
case 'neutral':
iconEmoji = 'undecided';
break;
default:
iconEmoji = user.emoji.status;
}
content = <span aria-label={user.emoji.status}><Icon iconName={iconEmoji} /></span>;
}
return content;
}
renderUserStatus() {
const user = this.props.user;
let userStatus;
const userStatusClasses = {};
userStatusClasses[styles.moderator] = user.isModerator;
userStatusClasses[styles.presenter] = user.isPresenter;
if (user.isModerator || user.isPresenter) {
userStatus = (
<span className={cx(styles.userStatus, userStatusClasses)} />
);
}
return userStatus;
}
renderUserMediaStatus() {
const user = this.props.user;
let userMediaStatus;
const userMediaClasses = {};
userMediaClasses[styles.voiceOnly] = user.isListenOnly;
userMediaClasses[styles.microphone] = user.isVoiceUser;
if (user.isListenOnly || user.isVoiceUser) {
userMediaStatus = (
<span className={cx(styles.userMediaStatus, userMediaClasses)}>
{user.isMuted ? <div className={styles.microphoneMuted} /> : null}
</span>
);
}
return userMediaStatus;
}
}
const UserAvatar = ({
children,
moderator,
presenter,
talking,
muted,
listenOnly,
color,
voice,
className,
}) => (
<div
className={cx(styles.avatar, {
[styles.moderator]: moderator,
[styles.presenter]: presenter,
[styles.muted]: muted,
[styles.listenOnly]: listenOnly,
[styles.talking]: talking,
[styles.voice]: voice,
}, className)}
style={{
backgroundColor: color,
color, // We need the same color on both for the border
}}
>
<div className={styles.content}>
{children}
</div>
</div>
);
UserAvatar.propTypes = propTypes;
UserAvatar.defaultProps = defaultProps;
export default UserAvatar;

View File

@ -5,103 +5,105 @@
*/
$user-avatar-border: $color-gray-light;
$user-avatar-text: $color-white;
$user-indicators-offset: -.15rem;
$user-indicator-presenter-bg: $color-primary;
$user-indicator-voice-bg: $color-success;
$user-indicator-muted-bg: $color-danger;
$user-list-bg: #F3F6F9;
$voice-user-bg: $color-success;
$voice-user-text: $color-gray-light;
$moderator-text: $color-white;
$moderator-bg: $color-primary;
.avatarText {
color: $user-avatar-text;
}
.userAvatar {
// @extend .flex-column;
flex-basis: 2.2rem;
height: 2.2rem;
flex-shrink: 0;
line-height: 2.2rem;
justify-content: center;
align-items: center;
.avatar {
position: relative;
display: flex;
flex-flow: column;
font-size: 1.1rem;
text-align: center;
width: 100%;
height: 0;
padding-bottom: 100%;
border-radius: 50%;
color: $color-white;
text-align: center;
text-transform: capitalize;
transition: box-shadow 0.3s;
transition: .3s ease-in-out;
font-size: 1rem;
&:after, &:before {
content: "";
position: absolute;
width: 0;
height: 0;
padding: .5rem;
color: inherit;
top: auto;
left: auto;
bottom: $user-indicators-offset / 2;
right: $user-indicators-offset;
border: 1.25px solid $user-list-bg;
border-radius: 50%;
background-color: $user-indicator-voice-bg;
color: $user-avatar-text;
opacity: 0;
transition: .3s ease-in-out;
font-family: 'bbb-icons';
font-size: .65rem;
line-height: 0;
text-align: center;
vertical-align: middle;
letter-spacing: -.65rem;
}
}
.userLogout {
flex-basis: 2.2rem;
height: 2.2rem;
flex-shrink: 0;
line-height: 2.2rem;
justify-content: center;
position: relative;
display: flex;
flex-flow: column;
font-size: 1.1rem;
text-align: center;
border-radius: 50%;
border: 1px solid $color-gray-lighter;
color: $color-gray-light;
font-style: italic;
text-transform: capitalize;
}
.userStatus {
position: absolute;
background-color: $color-white;
width: 0.625rem;
height: 0.625rem;
border-radius: 2px;
top: 0;
left: 0;
-webkit-box-shadow: 0 0 0 1px $color-white;
-moz-box-shadow: 0 0 0 1px $color-white;
box-shadow: 0 0 0 1px $color-white;
transition: all 0.3s;
.talking {
box-shadow: 0 0 0 1px $user-list-bg, 0 0 0 3px;
}
.moderator {
border: 1px solid $color-gray-light;
background-color: $color-white;
border-radius: 5px;
}
.presenter {
background-color: $moderator-bg;
border: none;
&:before {
content: "\00a0\e90b\00a0";
opacity: 1;
top: $user-indicators-offset / 2;
left: $user-indicators-offset;
bottom: auto;
right: auto;
border-radius: 5px;
background-color: $user-indicator-presenter-bg;
padding: .425rem;
}
}
.userMediaStatus {
display: flex;
justify-content: center;
.voice {
&:after {
content: "\00a0\e931\00a0";
background-color: $user-indicator-voice-bg;
opacity: 1;
}
}
.muted {
&:after {
content: "\00a0\e932\00a0";
background-color: $user-indicator-muted-bg;
opacity: 1;
}
}
.listenOnly {
&:after {
content: "\00a0\e90c\00a0";
opacity: 1;
}
}
.content {
color: $user-avatar-text;
top: 50%;
position: absolute;
border-radius: 50%;
width: 0.625rem;
height: 0.625rem;
top: 1.575rem;
left: 1.575rem;
-webkit-box-shadow: 0 0 0 1px $color-white;
-moz-box-shadow: 0 0 0 1px $color-white;
box-shadow: 0 0 0 1px $color-white;
transition: all 0.3s;
background-color: $color-success;
}
text-align: center;
left: 0;
right: 0;
font-size: 110%;
text-transform: capitalize;
.voiceOnly {
border: 1px solid $color-gray-light;
background-color: $color-white;
}
.microphoneMuted {
margin-top: 0.05rem;
width: 2px;
transform: rotate(45deg);
height: 0.5rem;
background-color: $color-white;
&, & > * {
line-height: 0; // to keep centralized vertically
}
}

View File

@ -67,7 +67,9 @@ class ChatListItem extends Component {
tabIndex={tabIndex}
>
<div className={styles.chatListItemLink}>
{chat.icon ? this.renderChatIcon() : this.renderChatAvatar()}
<div className={styles.chatIcon}>
{chat.icon ? this.renderChatIcon() : this.renderChatAvatar()}
</div>
<div className={styles.chatName}>
{!compact ? <span className={styles.chatNameMain}>{chat.name}</span> : null }
</div>
@ -89,7 +91,15 @@ class ChatListItem extends Component {
}
renderChatAvatar() {
return <UserAvatar user={this.props.chat} />;
const user = this.props.chat;
return (
<UserAvatar
moderator={user.isModerator}
color={user.color}
>
{user.name.toLowerCase().slice(0, 2)}
</UserAvatar>
);
}
renderChatIcon() {

View File

@ -48,6 +48,10 @@
font-size: 175%;
}
.chatIcon {
flex: 0 0 2.2rem;
}
.chatName {
@extend %flex-column;
@extend %text-elipsis;
@ -58,7 +62,7 @@
.chatNameMain {
@extend %no-margin;
@extend %text-elipsis;
font-size: 0.95rem;
font-size: 0.9rem;
font-weight: 400;
margin-left: $sm-padding-x;
}

View File

@ -1,20 +1,26 @@
import React, { Component } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router';
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
import styles from './styles.scss';
import cx from 'classnames';
import { defineMessages, injectIntl } from 'react-intl';
import UserListItem from './user-list-item/component.jsx';
import ChatListItem from './chat-list-item/component.jsx';
import KEY_CODES from '/imports/utils/keyCodes';
import Button from '/imports/ui/components/button/component';
import styles from './styles.scss';
import UserListItem from './user-list-item/component';
import ChatListItem from './chat-list-item/component';
const propTypes = {
openChats: PropTypes.array.isRequired,
users: PropTypes.array.isRequired,
compact: PropTypes.bool,
};
const defaultProps = {
compact: false,
};
const listTransition = {
@ -26,6 +32,49 @@ const listTransition = {
leaveActive: styles.leaveActive,
};
const intlMessages = defineMessages({
usersTitle: {
id: 'app.userlist.usersTitle',
description: 'Title for the Header',
},
messagesTitle: {
id: 'app.userlist.messagesTitle',
description: 'Title for the messages list',
},
participantsTitle: {
id: 'app.userlist.participantsTitle',
description: 'Title for the Users list',
},
toggleCompactView: {
id: 'app.userlist.toggleCompactView.label',
description: 'Toggle user list view mode',
},
ChatLabel: {
id: 'app.userlist.menu.chat.label',
description: 'Save the changes and close the settings menu',
},
ClearStatusLabel: {
id: 'app.userlist.menu.clearStatus.label',
description: 'Clear the emoji status of this user',
},
MakePresenterLabel: {
id: 'app.userlist.menu.makePresenter.label',
description: 'Set this user to be the presenter in this meeting',
},
KickUserLabel: {
id: 'app.userlist.menu.kickUser.label',
description: 'Forcefully remove this user from the meeting',
},
MuteUserAudioLabel: {
id: 'app.userlist.menu.muteUserAudio.label',
description: 'Forcefully mute this user',
},
UnmuteUserAudioLabel: {
id: 'app.userlist.menu.unmuteUserAudio.label',
description: 'Forcefully unmute this user',
},
});
class UserList extends Component {
constructor(props) {
super(props);
@ -33,6 +82,8 @@ class UserList extends Component {
compact: this.props.compact,
};
this.handleToggleCompactView = this.handleToggleCompactView.bind(this);
this.rovingIndex = this.rovingIndex.bind(this);
this.focusList = this.focusList.bind(this);
this.focusedItemIndex = -1;
@ -48,7 +99,7 @@ class UserList extends Component {
rovingIndex(event, listType) {
const { users, openChats } = this.props;
let active = document.activeElement;
const active = document.activeElement;
let list;
let items;
let numberOfItems;
@ -57,7 +108,7 @@ class UserList extends Component {
active.tabIndex = -1;
items.childNodes[this.focusedItemIndex].tabIndex = 0;
items.childNodes[this.focusedItemIndex].focus();
}
};
switch (listType) {
case 'users':
@ -70,6 +121,7 @@ class UserList extends Component {
items = this._msgItems;
numberOfItems = openChats.length;
break;
default: break;
}
if (event.keyCode === KEY_CODES.ESCAPE
@ -85,7 +137,7 @@ class UserList extends Component {
if (event.keyCode === KEY_CODES.ARROW_DOWN) {
this.focusedItemIndex += 1;
if (this.focusedItemIndex == numberOfItems) {
if (this.focusedItemIndex === numberOfItems) {
this.focusedItemIndex = 0;
}
focusElement();
@ -102,25 +154,20 @@ class UserList extends Component {
}
}
handleToggleCompactView() {
this.setState({ compact: !this.state.compact });
}
componentDidMount() {
if (!this.state.compact) {
this._msgsList.addEventListener('keydown',
event => this.rovingIndex(event, "messages"));
event => this.rovingIndex(event, 'messages'));
this._usersList.addEventListener('keydown',
event => this.rovingIndex(event, "users"));
event => this.rovingIndex(event, 'users'));
}
}
render() {
return (
<div className={styles.userList}>
{this.renderHeader()}
{this.renderContent()}
</div>
);
}
renderHeader() {
const { intl } = this.props;
@ -132,6 +179,13 @@ class UserList extends Component {
{intl.formatMessage(intlMessages.participantsTitle)}
</div> : null
}
{/* <Button
label={intl.formatMessage(intlMessages.toggleCompactView)}
hideLabel
icon={!this.state.compact ? 'left_arrow' : 'right_arrow'}
className={styles.btnToggle}
onClick={this.handleToggleCompactView}
/> */}
</div>
);
}
@ -167,8 +221,8 @@ class UserList extends Component {
>
<CSSTransitionGroup
transitionName={listTransition}
transitionAppear={true}
transitionEnter={true}
transitionAppear
transitionEnter
transitionLeave={false}
transitionAppearTimeout={0}
transitionEnterTimeout={0}
@ -252,9 +306,9 @@ class UserList extends Component {
>
<CSSTransitionGroup
transitionName={listTransition}
transitionAppear={true}
transitionEnter={true}
transitionLeave={true}
transitionAppear
transitionEnter
transitionLeave
transitionAppearTimeout={0}
transitionEnterTimeout={0}
transitionLeaveTimeout={0}
@ -281,46 +335,18 @@ class UserList extends Component {
</div>
);
}
render() {
return (
<div className={styles.userList}>
{this.renderHeader()}
{this.renderContent()}
</div>
);
}
}
const intlMessages = defineMessages({
usersTitle: {
id: 'app.userlist.usersTitle',
description: 'Title for the Header',
},
messagesTitle: {
id: 'app.userlist.messagesTitle',
description: 'Title for the messages list',
},
participantsTitle: {
id: 'app.userlist.participantsTitle',
description: 'Title for the Users list',
},
ChatLabel: {
id: 'app.userlist.menu.chat.label',
description: 'Save the changes and close the settings menu',
},
ClearStatusLabel: {
id: 'app.userlist.menu.clearStatus.label',
description: 'Clear the emoji status of this user',
},
MakePresenterLabel: {
id: 'app.userlist.menu.makePresenter.label',
description: 'Set this user to be the presenter in this meeting',
},
KickUserLabel: {
id: 'app.userlist.menu.kickUser.label',
description: 'Forcefully remove this user from the meeting',
},
MuteUserAudioLabel: {
id: 'app.userlist.menu.muteUserAudio.label',
description: 'Forcefully mute this user',
},
UnmuteUserAudioLabel: {
id: 'app.userlist.menu.unmuteUserAudio.label',
description: 'Forcefully unmute this user',
},
});
UserList.propTypes = propTypes;
UserList.defaultProps = defaultProps;
export default withRouter(injectIntl(UserList));

View File

@ -8,7 +8,6 @@ import UserList from './component';
const UserListContainer = (props) => {
const {
compact,
users,
currentUser,
openChats,
@ -21,7 +20,6 @@ const UserListContainer = (props) => {
return (
<UserList
compact={compact}
users={users}
meeting={meeting}
currentUser={currentUser}

View File

@ -16,9 +16,9 @@ const CLOSED_CHAT_LIST_KEY = 'closedChatList';
const mapOpenChats = (chat) => {
const currentUserId = Auth.userID;
return chat.message.fromUserId !== currentUserId
? chat.message.fromUserId
: chat.message.toUserId;
return chat.fromUserId !== currentUserId
? chat.fromUserId
: chat.toUserId;
};
const sortUsersByName = (a, b) => {
@ -152,7 +152,7 @@ const getUsers = () => {
const getOpenChats = (chatID) => {
let openChats = Chat
.find({ 'message.chat_type': PRIVATE_CHAT_TYPE })
.find({ type: PRIVATE_CHAT_TYPE })
.fetch()
.map(mapOpenChats);

View File

@ -4,10 +4,11 @@
/* Variables
* ==========
*/
$unread-messages-bg: $color-danger;
$user-list-bg: $color-white;
$user-list-text: $color-gray-dark;
$user-list-bg: #F3F6F9;
$user-list-text: $color-gray;
$list-item-bg-hover: darken($user-list-bg, 7%);
@ -77,15 +78,39 @@ $user-icons-color-hover: $color-gray;
.header {
@extend %flex-column;
justify-content: center;
justify-content: left;
flex-grow: 0;
display: flex;
flex-direction: row;
padding: 0 $md-padding-x;
margin: $md-padding-x 0;
}
.headerTitle {
font-size: 1.05rem;
flex: 0;
font-size: 1rem;
font-weight: 600;
padding: 0 $sm-padding-x;
margin: $lg-padding-x 0;
color: $color-heading;
}
.btnToggle {
flex: 0;
padding: 0;
margin: 0;
margin-left: auto;
background: transparent;
border: none;
box-shadow: none;
text-align: right;
& > i {
color: $color-heading;
text-align: right;
}
&:hover, &:focus, &:active {
background-color: transparent;
}
}
.content {
@ -109,12 +134,12 @@ $user-icons-color-hover: $color-gray;
}
.smallTitle {
color: $color-heading;
font-size: 0.9rem;
font-weight: 700;
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
padding: 0 $sm-padding-x;
margin: 0 0 ($lg-padding-x / 2) 0;
color: $color-gray-light;
}
.scrollableList {

View File

@ -16,6 +16,19 @@ import DropdownListSeparator from '/imports/ui/components/dropdown/list/separato
import DropdownListTitle from '/imports/ui/components/dropdown/list/title/component';
import styles from './styles.scss';
const normalizeEmojiName = (emoji) => {
const emojisNormalized = {
agree: 'thumbs_up',
disagree: 'thumbs_down',
thumbsUp: 'thumbs_up',
thumbsDown: 'thumbs_down',
raiseHand: 'hand',
away: 'time',
neutral: 'undecided',
};
return emoji in emojisNormalized ? emojisNormalized[emoji] : emoji;
};
const propTypes = {
user: PropTypes.shape({
@ -50,6 +63,10 @@ const messages = defineMessages({
id: 'app.userlist.locked',
description: 'Text for identifying locked user',
},
guest: {
id: 'app.userlist.guest',
description: 'Text for identifying guest user',
},
menuTitleContext: {
id: 'app.userlist.menuTitleContext',
description: 'adds context to userListItem menu title',
@ -217,66 +234,6 @@ class UserListItem extends Component {
});
}
renderUserContents() {
const {
user,
intl,
} = this.props;
const actions = this.getAvailableActions();
const contents = (
<div className={styles.userItemContents}>
<UserAvatar user={user} />
{this.renderUserName()}
{this.renderUserIcons()}
</div>
);
if (!actions.length) {
return contents;
}
const { dropdownOffset, dropdownDirection, dropdownVisible } = this.state;
return (
<Dropdown
ref={(ref) => { this.dropdown = ref; }}
isOpen={this.state.isActionsOpen}
onShow={this.onActionsShow}
onHide={this.onActionsHide}
className={styles.dropdown}
autoFocus={false}
>
<DropdownTrigger>
{contents}
</DropdownTrigger>
<DropdownContent
style={{
visibility: dropdownVisible ? 'visible' : 'hidden',
[dropdownDirection]: `${dropdownOffset}px`,
}}
className={styles.dropdownContent}
placement={`right ${dropdownDirection}`}
>
<DropdownList>
{
[
(<DropdownListTitle
description={intl.formatMessage(messages.menuTitleContext)}
key={_.uniqueId('dropdown-list-title')}
>
{user.name}
</DropdownListTitle>),
(<DropdownListSeparator key={_.uniqueId('action-separator')} />),
].concat(actions)
}
</DropdownList>
</DropdownContent>
</Dropdown>
);
}
renderUserName() {
const {
user,
@ -288,42 +245,31 @@ class UserListItem extends Component {
return null;
}
let userNameSub = [];
const userNameSub = [];
if (user.isPresenter) {
userNameSub.push(intl.formatMessage(messages.presenter));
if (user.isLocked) {
userNameSub.push(<span>
<Icon iconName="lock" />
{intl.formatMessage(messages.locked)}
</span>);
}
if (user.isCurrent) {
userNameSub.push(`(${intl.formatMessage(messages.you)})`);
if (user.isGuest) {
userNameSub.push(intl.formatMessage(messages.guest));
}
userNameSub = userNameSub.join(' ');
// FIX ME
/* const { disablePrivateChat,
disableCam,
disableMic,
disablePublicChat } = meeting.roomLockSettings;*/
const disablePrivateChat = false;
const disableCam = false;
const disableMic = false;
const disablePublicChat = false; // = meeting.roomLockSettings;
return (
<div className={styles.userName}>
<span className={styles.userNameMain}>
{user.name}
</span>
<span className={styles.userNameSub}>
{userNameSub}
{(user.isLocked) ?
<span> {(user.isCurrent ? ' | ' : null)}
<Icon iconName="lock" />
{intl.formatMessage(messages.locked)}
</span> : null}
{user.name} <i>{(user.isCurrent) ? `(${intl.formatMessage(messages.you)})` : ''}</i>
</span>
{
userNameSub.length ?
<span className={styles.userNameSub}>
{userNameSub.reduce((prev, curr) => [prev, ' | ', curr])}
</span>
: null
}
</div>
);
}
@ -338,22 +284,7 @@ class UserListItem extends Component {
return null;
}
let audioChatIcon = null;
if (user.isListenOnly) {
audioChatIcon = 'listen';
}
if (user.isVoiceUser) {
audioChatIcon = !user.isMuted ? 'unmute' : 'mute';
}
const audioIconClassnames = {};
audioIconClassnames[styles.userIconsContainer] = true;
audioIconClassnames[styles.userIconGlowing] = user.isTalking;
if (!audioChatIcon && !user.isSharingWebcam) {
if (!user.isSharingWebcam) {
// Prevent rendering the markup when there is no icon to show
return null;
}
@ -367,13 +298,6 @@ class UserListItem extends Component {
</span>
: null
}
{
audioChatIcon ?
<span className={cx(audioIconClassnames)}>
<Icon iconName={audioChatIcon} />
</span>
: null
}
</div>
);
}
@ -424,11 +348,25 @@ class UserListItem extends Component {
const actions = this.getAvailableActions();
const contents = (
<div
className={cx(styles.userListItem, userItemContentsStyle)}
className={!actions.length ? cx(styles.userListItem, userItemContentsStyle) : null}
aria-label={userAriaLabel}
>
<div className={styles.userItemContents} aria-hidden="true">
<UserAvatar user={user} />
<div className={styles.userAvatar}>
<UserAvatar
moderator={user.isModerator}
presenter={user.isPresenter}
talking={user.isTalking}
muted={user.isMuted}
listenOnly={user.isListenOnly}
voice={user.isVoiceUser}
color={user.color}
>
{user.emoji.status !== 'none' ?
<Icon iconName={normalizeEmojiName(user.emoji.status)} /> :
user.name.toLowerCase().slice(0, 2)}
</UserAvatar>
</div>
{this.renderUserName()}
{this.renderUserIcons()}
</div>
@ -447,7 +385,7 @@ class UserListItem extends Component {
isOpen={this.state.isActionsOpen}
onShow={this.onActionsShow}
onHide={this.onActionsHide}
className={styles.dropdown}
className={cx(styles.dropdown, styles.userListItem, userItemContentsStyle)}
autoFocus={false}
aria-haspopup="true"
aria-live="assertive"

View File

@ -5,19 +5,6 @@
flex-flow: column;
flex-shrink: 0;
.cursorPointer {
opacity: 0;
cursor: pointer;
transition: all 0.3s all;
}
&:hover,
&:focus {
.cursorPointer {
opacity: 1;
}
}
&:last-child {
margin-bottom: 0.5rem;
}
@ -28,7 +15,8 @@
outline: none;
}
.userItemContentsCompact {
.userAvatar {
flex: 0 0 2.2rem;
}
.userName {
@ -43,32 +31,27 @@
.userNameMain {
@extend %no-margin;
font-size: 0.9rem;
line-height: 1rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: all 0.3s;
font-weight: 400;
}
.hasSub {
transform: translateY(0%);
color: $color-gray-dark;
}
.userNameSub {
@extend %no-margin;
font-size: 0.8rem;
line-height: 0.9rem;
font-size: 0.75rem;
font-weight: 200;
font-style: italic;;
color: $sub-name-color;
color: $color-gray;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.userIconGlowing {
text-shadow: 0 0 .25rem;
i {
line-height: 0;
font-size: 75%;
}
}
.userIconsContainer {

View File

@ -32,18 +32,18 @@ class UnreadMessagesTracker {
count(chatID) {
const filter = {
'message.fromTime': {
fromTime: {
$gt: this.get(chatID),
},
'message.fromUserId': { $ne: Auth.userID },
fromUserId: { $ne: Auth.userID },
};
// Minimongo does not support $eq. See https://github.com/meteor/meteor/issues/4142
if (chatID === PUBLIC_CHAT_USERID) {
filter['message.toUserId'] = { $not: { $ne: chatID } };
filter.toUserId = { $not: { $ne: chatID } };
} else {
filter['message.toUserId'] = { $not: { $ne: Auth.userID } };
filter['message.fromUserId'].$not = { $ne: chatID };
filter.toUserId = { $not: { $ne: Auth.userID } };
filter.fromUserId.$not = { $ne: chatID };
}
return Chats.find(filter).count();

View File

@ -12,6 +12,8 @@ const mapUser = (user) => {
const mappedUser = {
id: user.userId,
name: user.name,
color: user.color,
avatar: user.avatar,
emoji: {
status: user.emoji,
changedAt: user.emojiTime,

View File

@ -40,6 +40,7 @@
"react-toggle": "~4.0.1",
"react-transition-group": "~1.1.3",
"redis": "^2.6.2",
"string-hash": "^1.1.3",
"winston": "^2.3.1",
"xml2js": "^0.4.17"
},

View File

@ -8,6 +8,8 @@ chat:
type_public: 'PUBLIC_CHAT'
type_private: 'PRIVATE_CHAT'
# Chat ids and names
system_userid: 'SYSTEM_MESSAGE'
system_username: 'SYSTEM_MESSAGE'
public_id: 'public'
public_userid: 'public_chat_userid'
public_username: 'public_chat_username'

View File

@ -7,6 +7,8 @@
"app.userlist.you": "You",
"app.userlist.locked": "Locked",
"app.userlist.Label": "User List",
"app.userlist.toggleCompactView.label": "Toggle compact view mode",
"app.userlist.guest": "Guest",
"app.chat.submitLabel": "Send Message",
"app.chat.inputLabel": "Message input for chat {0}",
"app.chat.inputPlaceholder": "Message {0}",