experiment
dist
node_modules
-devAssets
\ No newline at end of file
+devAssets
+
+.DS_Store
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
+<path d="M12,2L3,6v7c0,5,4,9,9,9c5,0,9-4,9-9V6L12,2z M19,13c0,3.9-3.1,7-7,7s-7-3.1-7-7V7.3l7-3.1l7,3.1V13z"/>
+</svg>
height: var(--av-extra-small);
}
-
img {
width: 100%;
height: 100%;
function Chip({
iconSrc, iconColor, text, children,
+ onClick,
}) {
return (
- <div className="chip">
- {iconSrc != null && <RawIcon src={iconSrc} color={iconColor} size="small" />}
- {(text != null && text !== '') && <Text variant="b2">{text}</Text>}
+ <button className="chip" type="button" onClick={onClick}>
+ {iconSrc != null && <RawIcon src={iconSrc} color={iconColor} size="extra-small" />}
+ {(text != null && text !== '') && <Text variant="b3">{text}</Text>}
{children}
- </div>
+ </button>
);
}
iconColor: PropTypes.string,
text: PropTypes.string,
children: PropTypes.element,
+ onClick: PropTypes.func,
};
Chip.defaultProps = {
iconColor: null,
text: null,
children: null,
+ onClick: null,
};
export default Chip;
background: var(--bg-surface-low);
border-radius: var(--bo-radius);
- border: 1px solid var(--bg-surface-border);
+ box-shadow: var(--bs-surface-border);
+ cursor: pointer;
+
+ @media (hover: hover) {
+ &:hover {
+ background-color: var(--bg-surface-hover);
+ }
+ }
+
+ & > .text {
+ flex: 1;
+ color: var(--tc-surface-high);
+ }
& > .ic-raw {
- margin-right: var(--sp-extra-tight);
+ width: 16px;
+ height: 16px;
+ margin-right: var(--sp-ultra-tight);
[dir=rtl] & {
margin-right: 0;
- margin-left: var(--sp-extra-tight);
+ margin-left: var(--sp-ultra-tight);
}
}
}
\ No newline at end of file
&__avatar-container {
padding-top: 6px;
- }
-
- &__avatar-container{
margin-right: var(--sp-tight);
+ & button {
+ cursor: pointer;
+ }
+
[dir=rtl] & {
margin: {
left: var(--sp-tight);
}
RoomIntro.defaultProps = {
- avatarSrc: false,
+ avatarSrc: null,
time: null,
};
--- /dev/null
+import React, { useState, useEffect, useRef } from 'react';
+import PropTypes from 'prop-types';
+import './ProfileViewer.scss';
+
+import initMatrix from '../../../client/initMatrix';
+import cons from '../../../client/state/cons';
+import navigation from '../../../client/state/navigation';
+import { selectRoom } from '../../../client/action/navigation';
+import * as roomActions from '../../../client/action/room';
+
+import { getUsername, getUsernameOfRoomMember, getPowerLabel } from '../../../util/matrixUtil';
+import colorMXID from '../../../util/colorMXID';
+
+import Text from '../../atoms/text/Text';
+import Chip from '../../atoms/chip/Chip';
+import IconButton from '../../atoms/button/IconButton';
+import Avatar from '../../atoms/avatar/Avatar';
+import Button from '../../atoms/button/Button';
+import Dialog from '../../molecules/dialog/Dialog';
+import SettingTile from '../../molecules/setting-tile/SettingTile';
+
+import ShieldEmptyIC from '../../../../public/res/ic/outlined/shield-empty.svg';
+import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
+import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
+
+function SessionInfo({ userId }) {
+ const [devices, setDevices] = useState(null);
+ const mx = initMatrix.matrixClient;
+
+ useEffect(() => {
+ let isUnmounted = false;
+
+ async function loadDevices() {
+ try {
+ await mx.downloadKeys([userId], true);
+ const myDevices = mx.getStoredDevicesForUser(userId);
+
+ if (isUnmounted) return;
+ setDevices(myDevices);
+ } catch {
+ setDevices([]);
+ }
+ }
+ loadDevices();
+
+ return () => {
+ isUnmounted = true;
+ };
+ }, [userId]);
+
+ function renderSessionChips() {
+ return (
+ <div className="session-info__chips">
+ {devices === null && <Text variant="b3">Loading sessions...</Text>}
+ {devices?.length === 0 && <Text variant="b3">No session found.</Text>}
+ {devices !== null && (devices.map((device) => (
+ <Chip
+ key={device.deviceId}
+ iconSrc={ShieldEmptyIC}
+ text={device.getDisplayName() || device.deviceId}
+ />
+ )))}
+ </div>
+ );
+ }
+
+ return (
+ <div className="session-info">
+ <SettingTile
+ title="Sessions"
+ content={renderSessionChips()}
+ />
+ </div>
+ );
+}
+
+SessionInfo.propTypes = {
+ userId: PropTypes.string.isRequired,
+};
+
+function ProfileFooter({ userId, onRequestClose }) {
+ const [isCreatingDM, setIsCreatingDM] = useState(false);
+ const [isIgnoring, setIsIgnoring] = useState(false);
+ const [isUserIgnored, setIsUserIgnored] = useState(initMatrix.matrixClient.isUserIgnored(userId));
+
+ const mx = initMatrix.matrixClient;
+ const isMountedRef = useRef(true);
+
+ useEffect(() => () => {
+ isMountedRef.current = false;
+ }, []);
+ useEffect(() => {
+ setIsUserIgnored(initMatrix.matrixClient.isUserIgnored(userId));
+ }, [userId]);
+
+ async function openDM() {
+ const directIds = [...initMatrix.roomList.directs];
+
+ // Check and open if user already have a DM with userId.
+ for (let i = 0; i < directIds.length; i += 1) {
+ const dRoom = mx.getRoom(directIds[i]);
+ const roomMembers = dRoom.getMembers();
+ if (roomMembers.length <= 2 && dRoom.currentState.members[userId]) {
+ selectRoom(directIds[i]);
+ onRequestClose();
+ return;
+ }
+ }
+
+ // Create new DM
+ try {
+ setIsCreatingDM(true);
+ const result = await roomActions.create({
+ isEncrypted: true,
+ isDirect: true,
+ invite: [userId],
+ });
+
+ if (isMountedRef.current === false) return;
+ setIsCreatingDM(false);
+ selectRoom(result.room_id);
+ onRequestClose();
+ } catch {
+ setIsCreatingDM(false);
+ }
+ }
+
+ async function toggleIgnore() {
+ const ignoredUsers = mx.getIgnoredUsers();
+ const uIndex = ignoredUsers.indexOf(userId);
+ if (uIndex >= 0) {
+ if (uIndex === -1) return;
+ ignoredUsers.splice(uIndex, 1);
+ } else ignoredUsers.push(userId);
+
+ try {
+ setIsIgnoring(true);
+ await mx.setIgnoredUsers(ignoredUsers);
+
+ if (isMountedRef.current === false) return;
+ setIsUserIgnored(uIndex < 0);
+ setIsIgnoring(false);
+ } catch {
+ setIsIgnoring(false);
+ }
+ }
+ return (
+ <div className="profile-viewer__buttons">
+ <Button
+ variant="primary"
+ onClick={openDM}
+ disabled={isCreatingDM}
+ >
+ {isCreatingDM ? 'Creating room...' : 'Message'}
+ </Button>
+ <Button>Mention</Button>
+ <Button
+ variant={isUserIgnored ? 'positive' : 'danger'}
+ onClick={toggleIgnore}
+ disabled={isIgnoring}
+ >
+ {
+ isUserIgnored
+ ? `${isIgnoring ? 'Unignoring...' : 'Unignore'}`
+ : `${isIgnoring ? 'Ignoring...' : 'Ignore'}`
+ }
+ </Button>
+ </div>
+ );
+}
+ProfileFooter.propTypes = {
+ userId: PropTypes.string.isRequired,
+ onRequestClose: PropTypes.func.isRequired,
+};
+
+function ProfileViewer() {
+ const [isOpen, setIsOpen] = useState(false);
+ const [roomId, setRoomId] = useState(null);
+ const [userId, setUserId] = useState(null);
+
+ const mx = initMatrix.matrixClient;
+ const room = roomId ? mx.getRoom(roomId) : null;
+ let username = '';
+ if (room !== null) {
+ const roomMember = room.getMember(userId);
+ if (roomMember) username = getUsernameOfRoomMember(roomMember);
+ else username = getUsername(userId);
+ }
+
+ function loadProfile(uId, rId) {
+ setIsOpen(true);
+ setUserId(uId);
+ setRoomId(rId);
+ }
+
+ useEffect(() => {
+ navigation.on(cons.events.navigation.PROFILE_VIEWER_OPENED, loadProfile);
+ return () => {
+ navigation.removeListener(cons.events.navigation.PROFILE_VIEWER_OPENED, loadProfile);
+ };
+ }, []);
+
+ useEffect(() => {
+ if (isOpen) return;
+ setUserId(null);
+ setRoomId(null);
+ }, [isOpen]);
+
+ function renderProfile() {
+ const member = room.getMember(userId) || mx.getUser(userId);
+ const avatarMxc = member.getMxcAvatarUrl() || member.avatarUrl;
+
+ return (
+ <div className="profile-viewer">
+ <div className="profile-viewer__user">
+ <Avatar
+ imageSrc={!avatarMxc ? null : mx.mxcUrlToHttp(avatarMxc, 80, 80, 'crop')}
+ text={username.slice(0, 1)}
+ bgColor={colorMXID(userId)}
+ size="large"
+ />
+ <div className="profile-viewer__user__info">
+ <Text variant="s1">{username}</Text>
+ <Text variant="b2">{userId}</Text>
+ </div>
+ <div className="profile-viewer__user__role">
+ <Text variant="b3">Role</Text>
+ <Button iconSrc={ChevronBottomIC}>{getPowerLabel(member.powerLevel) || 'Member'}</Button>
+ </div>
+ </div>
+ <SessionInfo userId={userId} />
+ { userId !== mx.getUserId() && (
+ <ProfileFooter
+ userId={userId}
+ onRequestClose={() => setIsOpen(false)}
+ />
+ )}
+ </div>
+ );
+ }
+
+ return (
+ <Dialog
+ className="profile-viewer__dialog"
+ isOpen={isOpen}
+ title={`${username} in ${room?.name ?? ''}`}
+ onRequestClose={() => setIsOpen(false)}
+ contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip="Close" />}
+ >
+ {isOpen && renderProfile()}
+ </Dialog>
+ );
+}
+
+export default ProfileViewer;
--- /dev/null
+.profile-viewer__dialog {
+ & .dialog__content__wrapper {
+ position: relative;
+ }
+ & .dialog__content-container {
+ padding: var(--sp-normal);
+ padding-bottom: 89px;
+ padding-right: var(--sp-extra-tight);
+ [dir=rtl] & {
+ padding-right: var(--sp-normal);
+ padding-left: var(--sp-extra-tight);
+ }
+ }
+}
+
+.profile-viewer {
+ &__user {
+ display: flex;
+ padding-bottom: var(--sp-normal);
+ border-bottom: 1px solid var(--bg-surface-border);
+
+ &__info {
+ align-self: end;
+ flex: 1;
+ min-width: 0;
+
+ margin: 0 var(--sp-normal);
+
+ & .text-s1 {
+ font-weight: 500;
+ }
+ & .text {
+ white-space: pre-wrap;
+ word-break: break-word;
+ }
+ }
+ &__role {
+ align-self: end;
+ & > .text {
+ margin-bottom: var(--sp-ultra-tight);
+ }
+ }
+ }
+
+ & .session-info {
+ margin-top: var(--sp-normal);
+ }
+
+ &__buttons {
+ position: absolute;
+ left: 0;
+ bottom: 0;
+
+ width: 100%;
+ padding: var(--sp-normal);
+ background-color: var(--bg-surface);
+ border-top: 1px solid var(--bg-surface-border);
+ display: flex;
+
+ & > *:nth-child(2n) {
+ margin: 0 var(--sp-normal)
+ }
+ & > *:last-child {
+ margin-left: auto;
+ [dir=rtl] & {
+ margin-left: 0;
+ margin-right: auto;
+ }
+ }
+ }
+}
+
+.session-info {
+ & .setting-tile__title .text {
+ color: var(--tc-surface-high);
+ }
+ &__chips {
+ padding-top: var(--sp-ultra-tight);
+ & .chip {
+ margin: {
+ top: var(--sp-extra-tight);
+ right: var(--sp-extra-tight);
+ }
+ [dir=rtl] & {
+ margin: 0 0 var(--sp-extra-tight) var(--sp-extra-tight);
+ }
+ }
+ }
+}
\ No newline at end of file
import React from 'react';
import ReadReceipts from '../read-receipts/ReadReceipts';
+import ProfileViewer from '../profile-viewer/ProfileViewer';
function Dialogs() {
return (
<>
<ReadReceipts />
+ <ProfileViewer />
</>
);
}
import Dialog from '../../molecules/dialog/Dialog';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
+import { openProfileViewer } from '../../../client/action/navigation';
function ReadReceipts() {
const [isOpen, setIsOpen] = useState(false);
return (
<PeopleSelector
key={receipt.userId}
- onClick={() => alert('Viewing profile is yet to be implemented')}
+ onClick={() => {
+ setIsOpen(false);
+ openProfileViewer(receipt.userId, roomId);
+ }}
avatarSrc={member?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 24, 24, 'crop')}
name={getUserDisplayName(receipt.userId)}
color={colorMXID(receipt.userId)}
import './PeopleDrawer.scss';
import initMatrix from '../../../client/initMatrix';
-import { getUsernameOfRoomMember } from '../../../util/matrixUtil';
+import { getPowerLabel, getUsernameOfRoomMember } from '../../../util/matrixUtil';
import colorMXID from '../../../util/colorMXID';
-import { openInviteUser } from '../../../client/action/navigation';
+import { openInviteUser, openProfileViewer } from '../../../client/action/navigation';
import Text from '../../atoms/text/Text';
import Header, { TitleWrapper } from '../../atoms/header/Header';
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
-function getPowerLabel(powerLevel) {
- if (powerLevel > 9000) return 'Goku';
- if (powerLevel > 100) return 'Founder';
- if (powerLevel === 100) return 'Admin';
- if (powerLevel >= 50) return 'Mod';
- return null;
-}
function AtoZ(m1, m2) {
const aName = m1.name;
const bName = m2.name;
memberList.map((member) => (
<PeopleSelector
key={member.userId}
- onClick={() => alert('Viewing profile is yet to be implemented')}
+ onClick={() => openProfileViewer(member.userId, roomId)}
avatarSrc={member.getAvatarUrl(initMatrix.matrixClient.baseUrl, 24, 24, 'crop')}
name={getUsernameOfRoomMember(member)}
color={colorMXID(member.userId)}
import { getUsername, getUsernameOfRoomMember, doesRoomHaveUnread } from '../../../util/matrixUtil';
import colorMXID from '../../../util/colorMXID';
import { diffMinutes, isNotInSameDay, getEventCords } from '../../../util/common';
-import { openEmojiBoard, openReadReceipts } from '../../../client/action/navigation';
+import { openEmojiBoard, openProfileViewer, openReadReceipts } from '../../../client/action/navigation';
import Divider from '../../atoms/divider/Divider';
import Avatar from '../../atoms/avatar/Avatar';
const senderMXIDColor = colorMXID(mEvent.sender.userId);
const userAvatar = isContentOnly ? null : (
- <Avatar
- imageSrc={mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop')}
- text={getUsernameOfRoomMember(mEvent.sender).slice(0, 1)}
- bgColor={senderMXIDColor}
- size="small"
- />
+ <button type="button" onClick={() => openProfileViewer(mEvent.sender.userId, roomId)}>
+ <Avatar
+ imageSrc={mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop')}
+ text={getUsernameOfRoomMember(mEvent.sender).slice(0, 1)}
+ bgColor={senderMXIDColor}
+ size="small"
+ />
+ </button>
);
const userHeader = isContentOnly ? null : (
<MessageHeader
});
}
+function openProfileViewer(userId, roomId) {
+ appDispatcher.dispatch({
+ type: cons.actions.navigation.OPEN_PROFILE_VIEWER,
+ userId,
+ roomId,
+ });
+}
+
function openSettings() {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_SETTINGS,
openPublicRooms,
openCreateRoom,
openInviteUser,
+ openProfileViewer,
openSettings,
openEmojiBoard,
openReadReceipts,
OPEN_PUBLIC_ROOMS: 'OPEN_PUBLIC_ROOMS',
OPEN_CREATE_ROOM: 'OPEN_CREATE_ROOM',
OPEN_INVITE_USER: 'OPEN_INVITE_USER',
+ OPEN_PROFILE_VIEWER: 'OPEN_PROFILE_VIEWER',
OPEN_SETTINGS: 'OPEN_SETTINGS',
OPEN_EMOJIBOARD: 'OPEN_EMOJIBOARD',
OPEN_READRECEIPTS: 'OPEN_READRECEIPTS',
CREATE_ROOM_OPENED: 'CREATE_ROOM_OPENED',
INVITE_USER_OPENED: 'INVITE_USER_OPENED',
SETTINGS_OPENED: 'SETTINGS_OPENED',
+ PROFILE_VIEWER_OPENED: 'PROFILE_VIEWER_OPENED',
EMOJIBOARD_OPENED: 'EMOJIBOARD_OPENED',
READRECEIPTS_OPENED: 'READRECEIPTS_OPENED',
ROOMOPTIONS_OPENED: 'ROOMOPTIONS_OPENED',
[cons.actions.navigation.OPEN_INVITE_USER]: () => {
this.emit(cons.events.navigation.INVITE_USER_OPENED, action.roomId, action.searchTerm);
},
+ [cons.actions.navigation.OPEN_PROFILE_VIEWER]: () => {
+ this.emit(cons.events.navigation.PROFILE_VIEWER_OPENED, action.userId, action.roomId);
+ },
[cons.actions.navigation.OPEN_SETTINGS]: () => {
this.emit(cons.events.navigation.SETTINGS_OPENED);
},
return true;
}
+function getPowerLabel(powerLevel) {
+ if (powerLevel > 9000) return 'Goku';
+ if (powerLevel > 100) return 'Founder';
+ if (powerLevel === 100) return 'Admin';
+ if (powerLevel >= 50) return 'Mod';
+ return null;
+}
+
export {
getBaseUrl, getUsername, getUsernameOfRoomMember,
- isRoomAliasAvailable, doesRoomHaveUnread,
+ isRoomAliasAvailable, doesRoomHaveUnread, getPowerLabel,
};