diff --git a/.gitignore b/.gitignore index 6fb5a7f..397d243 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ experiment dist node_modules -devAssets \ No newline at end of file +devAssets + +.DS_Store diff --git a/public/res/ic/outlined/shield-empty.svg b/public/res/ic/outlined/shield-empty.svg new file mode 100644 index 0000000..6bc9d30 --- /dev/null +++ b/public/res/ic/outlined/shield-empty.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/src/app/atoms/avatar/Avatar.scss b/src/app/atoms/avatar/Avatar.scss index d7ddc6e..42efbe7 100644 --- a/src/app/atoms/avatar/Avatar.scss +++ b/src/app/atoms/avatar/Avatar.scss @@ -24,7 +24,6 @@ height: var(--av-extra-small); } - img { width: 100%; height: 100%; diff --git a/src/app/atoms/chip/Chip.jsx b/src/app/atoms/chip/Chip.jsx index 3cededf..b21cf9e 100644 --- a/src/app/atoms/chip/Chip.jsx +++ b/src/app/atoms/chip/Chip.jsx @@ -7,13 +7,14 @@ import RawIcon from '../system-icons/RawIcon'; function Chip({ iconSrc, iconColor, text, children, + onClick, }) { return ( -
- {iconSrc != null && } - {(text != null && text !== '') && {text}} +
+ ); } @@ -22,6 +23,7 @@ Chip.propTypes = { iconColor: PropTypes.string, text: PropTypes.string, children: PropTypes.element, + onClick: PropTypes.func, }; Chip.defaultProps = { @@ -29,6 +31,7 @@ Chip.defaultProps = { iconColor: null, text: null, children: null, + onClick: null, }; export default Chip; diff --git a/src/app/atoms/chip/Chip.scss b/src/app/atoms/chip/Chip.scss index c53f5ba..8318fcf 100644 --- a/src/app/atoms/chip/Chip.scss +++ b/src/app/atoms/chip/Chip.scss @@ -7,13 +7,27 @@ 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 diff --git a/src/app/molecules/message/Message.scss b/src/app/molecules/message/Message.scss index 60886ca..2b757b0 100644 --- a/src/app/molecules/message/Message.scss +++ b/src/app/molecules/message/Message.scss @@ -22,11 +22,12 @@ &__avatar-container { padding-top: 6px; - } - - &__avatar-container{ margin-right: var(--sp-tight); + & button { + cursor: pointer; + } + [dir=rtl] & { margin: { left: var(--sp-tight); diff --git a/src/app/molecules/room-intro/RoomIntro.jsx b/src/app/molecules/room-intro/RoomIntro.jsx index df5618d..5c437d7 100644 --- a/src/app/molecules/room-intro/RoomIntro.jsx +++ b/src/app/molecules/room-intro/RoomIntro.jsx @@ -28,7 +28,7 @@ function RoomIntro({ } RoomIntro.defaultProps = { - avatarSrc: false, + avatarSrc: null, time: null, }; diff --git a/src/app/organisms/profile-viewer/ProfileViewer.jsx b/src/app/organisms/profile-viewer/ProfileViewer.jsx new file mode 100644 index 0000000..17405e9 --- /dev/null +++ b/src/app/organisms/profile-viewer/ProfileViewer.jsx @@ -0,0 +1,255 @@ +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 ( +
+ {devices === null && Loading sessions...} + {devices?.length === 0 && No session found.} + {devices !== null && (devices.map((device) => ( + + )))} +
+ ); + } + + return ( +
+ +
+ ); +} + +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 ( +
+ + + +
+ ); +} +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 ( +
+
+ +
+ {username} + {userId} +
+
+ Role + +
+
+ + { userId !== mx.getUserId() && ( + setIsOpen(false)} + /> + )} +
+ ); + } + + return ( + setIsOpen(false)} + contentOptions={ setIsOpen(false)} tooltip="Close" />} + > + {isOpen && renderProfile()} + + ); +} + +export default ProfileViewer; diff --git a/src/app/organisms/profile-viewer/ProfileViewer.scss b/src/app/organisms/profile-viewer/ProfileViewer.scss new file mode 100644 index 0000000..b10b1aa --- /dev/null +++ b/src/app/organisms/profile-viewer/ProfileViewer.scss @@ -0,0 +1,89 @@ +.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 diff --git a/src/app/organisms/pw/Dialogs.jsx b/src/app/organisms/pw/Dialogs.jsx index d878ed9..3448ebe 100644 --- a/src/app/organisms/pw/Dialogs.jsx +++ b/src/app/organisms/pw/Dialogs.jsx @@ -1,11 +1,13 @@ import React from 'react'; import ReadReceipts from '../read-receipts/ReadReceipts'; +import ProfileViewer from '../profile-viewer/ProfileViewer'; function Dialogs() { return ( <> + ); } diff --git a/src/app/organisms/read-receipts/ReadReceipts.jsx b/src/app/organisms/read-receipts/ReadReceipts.jsx index ea6ae28..689a045 100644 --- a/src/app/organisms/read-receipts/ReadReceipts.jsx +++ b/src/app/organisms/read-receipts/ReadReceipts.jsx @@ -11,6 +11,7 @@ import PeopleSelector from '../../molecules/people-selector/PeopleSelector'; 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); @@ -58,7 +59,10 @@ function ReadReceipts() { return ( 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)} diff --git a/src/app/organisms/room/PeopleDrawer.jsx b/src/app/organisms/room/PeopleDrawer.jsx index ca975d1..205047d 100644 --- a/src/app/organisms/room/PeopleDrawer.jsx +++ b/src/app/organisms/room/PeopleDrawer.jsx @@ -3,9 +3,9 @@ import PropTypes from 'prop-types'; 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'; @@ -17,13 +17,6 @@ import PeopleSelector from '../../molecules/people-selector/PeopleSelector'; 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; @@ -88,7 +81,7 @@ function PeopleDrawer({ roomId }) { memberList.map((member) => ( 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)} diff --git a/src/app/organisms/room/RoomViewContent.jsx b/src/app/organisms/room/RoomViewContent.jsx index efa8318..170d25d 100644 --- a/src/app/organisms/room/RoomViewContent.jsx +++ b/src/app/organisms/room/RoomViewContent.jsx @@ -11,7 +11,7 @@ import { redactEvent, sendReaction } from '../../../client/action/roomTimeline'; 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'; @@ -353,12 +353,14 @@ function RoomViewContent({ const senderMXIDColor = colorMXID(mEvent.sender.userId); const userAvatar = isContentOnly ? null : ( - + ); const userHeader = isContentOnly ? null : ( { 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); }, diff --git a/src/util/matrixUtil.js b/src/util/matrixUtil.js index 1dbd5fb..e40fa73 100644 --- a/src/util/matrixUtil.js +++ b/src/util/matrixUtil.js @@ -69,7 +69,15 @@ function doesRoomHaveUnread(room) { 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, };