Merge pull request #4157 from oswaldoacauan/userlist-2x-design
[HTML5 2.0] User-list Redesign
This commit is contained in:
commit
34a3afc26b
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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})`));
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
|
@ -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);
|
||||
|
@ -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),
|
||||
),
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,7 +84,7 @@ class ChatDropdown extends Component {
|
||||
const link = document.createElement('a');
|
||||
const mimeType = 'text/plain';
|
||||
|
||||
link.setAttribute('download', 'chat.txt');
|
||||
link.setAttribute('download', `public-chat-${Date.now()}.txt`);
|
||||
link.setAttribute('href', `data: ${mimeType} ;charset=utf-8,
|
||||
${encodeURIComponent(ChatService.exportChat(ChatService.getPublicMessages()))}`);
|
||||
link.click();
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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'
|
||||
|
@ -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}",
|
||||
|
Loading…
Reference in New Issue
Block a user