Implement Profile Viewer (#130)
authorGero Gerke <hello@gerogerke.de>
Mon, 18 Oct 2021 15:25:52 +0000 (17:25 +0200)
committerGitHub <noreply@github.com>
Mon, 18 Oct 2021 15:25:52 +0000 (20:55 +0530)
* Implement Profile Viewer

Fixes #111

* Make user avatar in chat clickable

* design progress

* Refactored code

* progress

* Updated chip comp

Signed-off-by: Ajay Bura <ajbura@gmail.com>
* Refactored ProfileViewer comp

Signed-off-by: Ajay Bura <ajbura@gmail.com>
* Added msg functionality in ProfileViewer

Signed-off-by: Ajay Bura <ajbura@gmail.com>
* Added Ignore functionality in ProfileViewer

Signed-off-by: Ajay Bura <ajbura@gmail.com>
* Fixed Ignore btn bug

Signed-off-by: Ajay Bura <ajbura@gmail.com>
* Refectored ProfileViewer comp

Signed-off-by: Ajay Bura <ajbura@gmail.com>
Co-authored-by: Ajay Bura <ajbura@gmail.com>
17 files changed:
.gitignore
public/res/ic/outlined/shield-empty.svg [new file with mode: 0644]
src/app/atoms/avatar/Avatar.scss
src/app/atoms/chip/Chip.jsx
src/app/atoms/chip/Chip.scss
src/app/molecules/message/Message.scss
src/app/molecules/room-intro/RoomIntro.jsx
src/app/organisms/profile-viewer/ProfileViewer.jsx [new file with mode: 0644]
src/app/organisms/profile-viewer/ProfileViewer.scss [new file with mode: 0644]
src/app/organisms/pw/Dialogs.jsx
src/app/organisms/read-receipts/ReadReceipts.jsx
src/app/organisms/room/PeopleDrawer.jsx
src/app/organisms/room/RoomViewContent.jsx
src/client/action/navigation.js
src/client/state/cons.js
src/client/state/navigation.js
src/util/matrixUtil.js

index 6fb5a7fe214e06857c41860854156c965518fefd..397d2434d9ddfade1c9ed1d261b54a0f8f6b7671 100644 (file)
@@ -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 (file)
index 0000000..6bc9d30
--- /dev/null
@@ -0,0 +1,7 @@
+<?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>
index d7ddc6e8acb4fe184b562fee4f76fb87b26db4ce..42efbe790eaa259c4af7e66dda8b7c2ebe8babe6 100644 (file)
@@ -24,7 +24,6 @@
     height: var(--av-extra-small);
   }
 
-
   img {
     width: 100%;
     height: 100%;
index 3cededffad0b7598ce4e6cd1d5fecb8c00a82225..b21cf9ed1102159b333a6b227ce26bc975c0060f 100644 (file)
@@ -7,13 +7,14 @@ import RawIcon from '../system-icons/RawIcon';
 
 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>
   );
 }
 
@@ -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;
index c53f5ba7ad530ba1e8addbaf2cc163138e449a8d..8318fcf6dbba35ca08429f1305721eb2dee305a0 100644 (file)
@@ -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
index 60886ca985f7163ab36495ae26159b320dd8e774..2b757b08c443a05ccfbe11da0eb7d684e3d7aca4 100644 (file)
   
   &__avatar-container {
     padding-top: 6px;
-  }
-  
-  &__avatar-container{
     margin-right: var(--sp-tight);
 
+    & button {
+      cursor: pointer;
+    }
+
     [dir=rtl] & {
       margin: {
         left: var(--sp-tight);
index df5618dca3eacc7863bdae37380ca7f33daec33c..5c437d792d0982c471670fa84c561f903ff0b1ce 100644 (file)
@@ -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 (file)
index 0000000..17405e9
--- /dev/null
@@ -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 (
+      <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;
diff --git a/src/app/organisms/profile-viewer/ProfileViewer.scss b/src/app/organisms/profile-viewer/ProfileViewer.scss
new file mode 100644 (file)
index 0000000..b10b1aa
--- /dev/null
@@ -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
index d878ed9c3c035111093fda5587bf26fbc423a67d..3448ebe95a37674f0df3fca2644bcd09a6ccc00f 100644 (file)
@@ -1,11 +1,13 @@
 import React from 'react';
 
 import ReadReceipts from '../read-receipts/ReadReceipts';
+import ProfileViewer from '../profile-viewer/ProfileViewer';
 
 function Dialogs() {
   return (
     <>
       <ReadReceipts />
+      <ProfileViewer />
     </>
   );
 }
index ea6ae28663e4a2cf19bcbb0910bc76e701275b9f..689a045b33156a4eb69d11924eca0876a94db234 100644 (file)
@@ -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 (
       <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)}
index ca975d13c1d46c451471fda10a143a1d279d50e8..205047dcba1df9b833ab697ab23bfb1267315f84 100644 (file)
@@ -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) => (
                   <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)}
index efa8318cf1067a1a07375eba095113b8af1baa09..170d25d17bb18ae9754efcc4fc76b25fcbbe0cbd 100644 (file)
@@ -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 : (
-      <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
index d11aceb7204e4fe598dadae6104eb0272e970501..b9e428fdb6a8434934bf50539dafa3f3b49485db 100644 (file)
@@ -55,6 +55,14 @@ function openInviteUser(roomId, searchTerm) {
   });
 }
 
+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,
@@ -94,6 +102,7 @@ export {
   openPublicRooms,
   openCreateRoom,
   openInviteUser,
+  openProfileViewer,
   openSettings,
   openEmojiBoard,
   openReadReceipts,
index fee81b5ede7cb8342f5efdaadc8a18cccd238387..6cd177e2e26b2a05ec573405d51412ae16feabac 100644 (file)
@@ -27,6 +27,7 @@ const cons = {
       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',
@@ -57,6 +58,7 @@ const cons = {
       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',
index d7dabd7851eb5c19ec119f1cab668c2b835bde1f..cbf8a764da476167150b42ed02f13e7aaffb31dc 100644 (file)
@@ -69,6 +69,9 @@ class Navigation extends EventEmitter {
       [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);
       },
index 1dbd5fb14dddeabf52754225f26453ab069507e3..e40fa73ce6815dd83ff6081340562a03fd412bae 100644 (file)
@@ -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,
 };