2021-10-18 17:25:52 +02:00
import React , { useState , useEffect , useRef } from 'react' ;
import PropTypes from 'prop-types' ;
import './ProfileViewer.scss' ;
2021-11-23 11:56:02 +05:30
import { twemojify } from '../../../util/twemojify' ;
2021-10-18 17:25:52 +02:00
import initMatrix from '../../../client/initMatrix' ;
import cons from '../../../client/state/cons' ;
import navigation from '../../../client/state/navigation' ;
2022-01-12 13:57:13 +05:30
import { selectRoom , openReusableContextMenu } from '../../../client/action/navigation' ;
2021-10-18 17:25:52 +02:00
import * as roomActions from '../../../client/action/room' ;
2022-02-05 19:25:59 +05:30
import {
2022-07-08 20:24:35 +05:30
getUsername , getUsernameOfRoomMember , getPowerLabel , hasDMWith , hasDevices
2022-02-05 19:25:59 +05:30
} from '../../../util/matrixUtil' ;
2022-01-12 13:57:13 +05:30
import { getEventCords } from '../../../util/common' ;
2021-10-18 17:25:52 +02:00
import colorMXID from '../../../util/colorMXID' ;
import Text from '../../atoms/text/Text' ;
import Chip from '../../atoms/chip/Chip' ;
import IconButton from '../../atoms/button/IconButton' ;
2022-01-12 18:26:52 +05:30
import Input from '../../atoms/input/Input' ;
2021-10-18 17:25:52 +02:00
import Avatar from '../../atoms/avatar/Avatar' ;
import Button from '../../atoms/button/Button' ;
2022-01-13 09:42:23 +05:30
import { MenuItem } from '../../atoms/context-menu/ContextMenu' ;
2022-01-12 13:57:13 +05:30
import PowerLevelSelector from '../../molecules/power-level-selector/PowerLevelSelector' ;
2021-10-18 17:25:52 +02:00
import Dialog from '../../molecules/dialog/Dialog' ;
import ShieldEmptyIC from '../../../../public/res/ic/outlined/shield-empty.svg' ;
2022-01-13 09:42:23 +05:30
import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg' ;
2021-10-18 17:25:52 +02:00
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg' ;
import CrossIC from '../../../../public/res/ic/outlined/cross.svg' ;
2022-01-12 13:57:13 +05:30
import { useForceUpdate } from '../../hooks/useForceUpdate' ;
2022-04-25 20:21:21 +05:30
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog' ;
2022-01-12 13:57:13 +05:30
2022-01-12 18:26:52 +05:30
function ModerationTools ( {
roomId , userId ,
} ) {
const mx = initMatrix . matrixClient ;
const room = mx . getRoom ( roomId ) ;
const roomMember = room . getMember ( userId ) ;
2022-01-16 18:17:20 +05:30
const myPowerLevel = room . getMember ( mx . getUserId ( ) ) ? . powerLevel || 0 ;
2022-01-12 18:26:52 +05:30
const powerLevel = roomMember ? . powerLevel || 0 ;
const canIKick = (
roomMember ? . membership === 'join'
&& room . currentState . hasSufficientPowerLevelFor ( 'kick' , myPowerLevel )
&& powerLevel < myPowerLevel
) ;
2022-01-12 18:50:54 +05:30
const canIBan = (
[ 'join' , 'leave' ] . includes ( roomMember ? . membership )
&& room . currentState . hasSufficientPowerLevelFor ( 'ban' , myPowerLevel )
&& powerLevel < myPowerLevel
) ;
2022-01-12 18:26:52 +05:30
const handleKick = ( e ) => {
e . preventDefault ( ) ;
const kickReason = e . target . elements [ 'kick-reason' ] ? . value . trim ( ) ;
roomActions . kick ( roomId , userId , kickReason !== '' ? kickReason : undefined ) ;
} ;
2022-01-12 18:50:54 +05:30
const handleBan = ( e ) => {
e . preventDefault ( ) ;
const banReason = e . target . elements [ 'ban-reason' ] ? . value . trim ( ) ;
roomActions . ban ( roomId , userId , banReason !== '' ? banReason : undefined ) ;
} ;
2022-01-12 18:26:52 +05:30
return (
< div className = "moderation-tools" >
{ canIKick && (
2022-01-12 18:50:54 +05:30
< form onSubmit = { handleKick } >
< Input label = "Kick reason" name = "kick-reason" / >
< Button type = "submit" > Kick < / Button >
< / form >
) }
{ canIBan && (
< form onSubmit = { handleBan } >
< Input label = "Ban reason" name = "ban-reason" / >
< Button type = "submit" > Ban < / Button >
< / form >
2022-01-12 18:26:52 +05:30
) }
< / div >
) ;
}
ModerationTools . propTypes = {
roomId : PropTypes . string . isRequired ,
userId : PropTypes . string . isRequired ,
} ;
2021-10-18 17:25:52 +02:00
function SessionInfo ( { userId } ) {
const [ devices , setDevices ] = useState ( null ) ;
2022-01-13 09:42:23 +05:30
const [ isVisible , setIsVisible ] = useState ( false ) ;
2021-10-18 17:25:52 +02:00
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 ( ) {
2022-01-13 09:42:23 +05:30
if ( ! isVisible ) return null ;
2021-10-18 17:25:52 +02:00
return (
< div className = "session-info__chips" >
2022-01-13 09:42:23 +05:30
{ devices === null && < Text variant = "b2" > Loading sessions ... < / Text > }
{ devices ? . length === 0 && < Text variant = "b2" > No session found . < / Text > }
2021-10-18 17:25:52 +02:00
{ devices !== null && ( devices . map ( ( device ) => (
< Chip
key = { device . deviceId }
iconSrc = { ShieldEmptyIC }
text = { device . getDisplayName ( ) || device . deviceId }
/ >
) ) ) }
< / div >
) ;
}
return (
< div className = "session-info" >
2022-01-13 09:42:23 +05:30
< MenuItem
onClick = { ( ) => setIsVisible ( ! isVisible ) }
iconSrc = { isVisible ? ChevronBottomIC : ChevronRightIC }
>
< Text variant = "b2" > { ` View ${ devices ? . length > 0 ? ` ${ devices . length } ` : '' } sessions ` } < / Text >
< / MenuItem >
{ renderSessionChips ( ) }
2021-10-18 17:25:52 +02:00
< / div >
) ;
}
SessionInfo . propTypes = {
userId : PropTypes . string . isRequired ,
} ;
2021-10-29 17:13:33 +05:30
function ProfileFooter ( { roomId , userId , onRequestClose } ) {
2021-10-18 17:25:52 +02:00
const [ isCreatingDM , setIsCreatingDM ] = useState ( false ) ;
const [ isIgnoring , setIsIgnoring ] = useState ( false ) ;
const [ isUserIgnored , setIsUserIgnored ] = useState ( initMatrix . matrixClient . isUserIgnored ( userId ) ) ;
const isMountedRef = useRef ( true ) ;
2021-10-29 17:13:33 +05:30
const mx = initMatrix . matrixClient ;
const room = mx . getRoom ( roomId ) ;
const member = room . getMember ( userId ) ;
const isInvitable = member ? . membership !== 'join' && member ? . membership !== 'ban' ;
const [ isInviting , setIsInviting ] = useState ( false ) ;
const [ isInvited , setIsInvited ] = useState ( member ? . membership === 'invite' ) ;
2021-10-18 17:25:52 +02:00
2022-01-16 18:17:20 +05:30
const myPowerlevel = room . getMember ( mx . getUserId ( ) ) ? . powerLevel || 0 ;
2021-12-03 18:32:10 +05:30
const userPL = room . getMember ( userId ) ? . powerLevel || 0 ;
2021-11-24 10:08:51 +05:30
const canIKick = room . currentState . hasSufficientPowerLevelFor ( 'kick' , myPowerlevel ) && userPL < myPowerlevel ;
2021-10-29 18:11:02 +05:30
2022-01-13 10:28:33 +05:30
const isBanned = member ? . membership === 'ban' ;
2021-11-23 16:24:12 +05:30
const onCreated = ( dmRoomId ) => {
if ( isMountedRef . current === false ) return ;
setIsCreatingDM ( false ) ;
selectRoom ( dmRoomId ) ;
onRequestClose ( ) ;
} ;
2021-11-24 10:08:51 +05:30
useEffect ( ( ) => {
2021-11-23 16:24:12 +05:30
const { roomList } = initMatrix ;
roomList . on ( cons . events . roomList . ROOM _CREATED , onCreated ) ;
return ( ) => {
2021-11-24 10:08:51 +05:30
isMountedRef . current = false ;
2021-11-23 16:24:12 +05:30
roomList . removeListener ( cons . events . roomList . ROOM _CREATED , onCreated ) ;
} ;
2021-10-18 17:25:52 +02:00
} , [ ] ) ;
useEffect ( ( ) => {
setIsUserIgnored ( initMatrix . matrixClient . isUserIgnored ( userId ) ) ;
2021-10-29 17:13:33 +05:30
setIsIgnoring ( false ) ;
setIsInviting ( false ) ;
2021-10-18 17:25:52 +02:00
} , [ userId ] ) ;
2022-01-13 10:28:33 +05:30
const openDM = async ( ) => {
2021-10-18 17:25:52 +02:00
// Check and open if user already have a DM with userId.
2022-02-05 19:25:59 +05:30
const dmRoomId = hasDMWith ( userId ) ;
if ( dmRoomId ) {
selectRoom ( dmRoomId ) ;
onRequestClose ( ) ;
return ;
2021-10-18 17:25:52 +02:00
}
// Create new DM
try {
setIsCreatingDM ( true ) ;
2022-07-08 20:24:35 +05:30
await roomActions . createDM ( userId , await hasDevices ( userId ) ) ;
2021-10-18 17:25:52 +02:00
} catch {
2021-11-23 16:24:12 +05:30
if ( isMountedRef . current === false ) return ;
2021-10-18 17:25:52 +02:00
setIsCreatingDM ( false ) ;
}
2022-01-13 10:28:33 +05:30
} ;
2021-10-18 17:25:52 +02:00
2022-01-13 10:28:33 +05:30
const toggleIgnore = async ( ) => {
2021-10-18 17:25:52 +02:00
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 ) ;
}
2022-01-13 10:28:33 +05:30
} ;
2021-10-29 17:13:33 +05:30
2022-01-13 10:28:33 +05:30
const toggleInvite = async ( ) => {
2021-10-29 17:13:33 +05:30
try {
setIsInviting ( true ) ;
let isInviteSent = false ;
if ( isInvited ) await roomActions . kick ( roomId , userId ) ;
else {
await roomActions . invite ( roomId , userId ) ;
isInviteSent = true ;
}
if ( isMountedRef . current === false ) return ;
setIsInvited ( isInviteSent ) ;
setIsInviting ( false ) ;
} catch {
setIsInviting ( false ) ;
}
2022-01-13 10:28:33 +05:30
} ;
2021-10-29 17:13:33 +05:30
2021-10-18 17:25:52 +02:00
return (
< div className = "profile-viewer__buttons" >
< Button
variant = "primary"
onClick = { openDM }
disabled = { isCreatingDM }
>
{ isCreatingDM ? 'Creating room...' : 'Message' }
< / Button >
2022-01-13 10:28:33 +05:30
{ isBanned && canIKick && (
< Button
variant = "positive"
onClick = { ( ) => roomActions . unban ( roomId , userId ) }
>
Unban
< / Button >
) }
2021-10-29 18:11:02 +05:30
{ ( isInvited ? canIKick : room . canInvite ( mx . getUserId ( ) ) ) && isInvitable && (
< Button
onClick = { toggleInvite }
disabled = { isInviting }
>
2021-10-29 17:13:33 +05:30
{
isInvited
? ` ${ isInviting ? 'Disinviting...' : 'Disinvite' } `
: ` ${ isInviting ? 'Inviting...' : 'Invite' } `
}
< / Button >
) }
2021-10-18 17:25:52 +02:00
< Button
variant = { isUserIgnored ? 'positive' : 'danger' }
onClick = { toggleIgnore }
disabled = { isIgnoring }
>
{
isUserIgnored
? ` ${ isIgnoring ? 'Unignoring...' : 'Unignore' } `
: ` ${ isIgnoring ? 'Ignoring...' : 'Ignore' } `
}
< / Button >
< / div >
) ;
}
ProfileFooter . propTypes = {
2021-10-29 17:13:33 +05:30
roomId : PropTypes . string . isRequired ,
2021-10-18 17:25:52 +02:00
userId : PropTypes . string . isRequired ,
onRequestClose : PropTypes . func . isRequired ,
} ;
2022-01-12 16:46:56 +05:30
function useToggleDialog ( ) {
2021-10-18 17:25:52 +02:00
const [ isOpen , setIsOpen ] = useState ( false ) ;
const [ roomId , setRoomId ] = useState ( null ) ;
const [ userId , setUserId ] = useState ( null ) ;
useEffect ( ( ) => {
2022-01-12 13:57:13 +05:30
const loadProfile = ( uId , rId ) => {
setIsOpen ( true ) ;
setUserId ( uId ) ;
setRoomId ( rId ) ;
} ;
2021-10-18 17:25:52 +02:00
navigation . on ( cons . events . navigation . PROFILE _VIEWER _OPENED , loadProfile ) ;
return ( ) => {
navigation . removeListener ( cons . events . navigation . PROFILE _VIEWER _OPENED , loadProfile ) ;
} ;
} , [ ] ) ;
2022-01-12 16:46:56 +05:30
const closeDialog = ( ) => setIsOpen ( false ) ;
const afterClose = ( ) => {
setUserId ( null ) ;
setRoomId ( null ) ;
} ;
return [ isOpen , roomId , userId , closeDialog , afterClose ] ;
}
2022-01-12 18:26:52 +05:30
function useRerenderOnProfileChange ( roomId , userId ) {
2022-01-12 16:46:56 +05:30
const mx = initMatrix . matrixClient ;
const [ , forceUpdate ] = useForceUpdate ( ) ;
2022-01-12 13:57:13 +05:30
useEffect ( ( ) => {
2022-01-12 18:26:52 +05:30
const handleProfileChange = ( mEvent , member ) => {
if (
mEvent . getRoomId ( ) === roomId
&& ( member . userId === userId || member . userId === mx . getUserId ( ) )
) {
2022-01-12 13:57:13 +05:30
forceUpdate ( ) ;
}
} ;
2022-01-12 18:26:52 +05:30
mx . on ( 'RoomMember.powerLevel' , handleProfileChange ) ;
mx . on ( 'RoomMember.membership' , handleProfileChange ) ;
2022-01-12 13:57:13 +05:30
return ( ) => {
2022-01-12 18:26:52 +05:30
mx . removeListener ( 'RoomMember.powerLevel' , handleProfileChange ) ;
mx . removeListener ( 'RoomMember.membership' , handleProfileChange ) ;
2022-01-12 13:57:13 +05:30
} ;
} , [ roomId , userId ] ) ;
2022-01-12 16:46:56 +05:30
}
2022-01-12 13:57:13 +05:30
2022-01-12 16:46:56 +05:30
function ProfileViewer ( ) {
const [ isOpen , roomId , userId , closeDialog , handleAfterClose ] = useToggleDialog ( ) ;
2022-01-12 18:26:52 +05:30
useRerenderOnProfileChange ( roomId , userId ) ;
2022-01-12 16:46:56 +05:30
const mx = initMatrix . matrixClient ;
const room = mx . getRoom ( roomId ) ;
2021-10-18 17:25:52 +02:00
2022-01-12 16:46:56 +05:30
const renderProfile = ( ) => {
2022-01-13 09:42:23 +05:30
const roomMember = room . getMember ( userId ) ;
const username = roomMember ? getUsernameOfRoomMember ( roomMember ) : getUsername ( userId ) ;
2022-01-13 10:33:04 +05:30
const avatarMxc = roomMember ? . getMxcAvatarUrl ? . ( ) || mx . getUser ( userId ) ? . avatarUrl ;
2022-01-12 18:26:52 +05:30
const avatarUrl = ( avatarMxc && avatarMxc !== 'null' ) ? mx . mxcUrlToHttp ( avatarMxc , 80 , 80 , 'crop' ) : null ;
2022-01-12 16:46:56 +05:30
2022-01-16 18:17:20 +05:30
const powerLevel = roomMember ? . powerLevel || 0 ;
2022-01-12 13:57:13 +05:30
const myPowerLevel = room . getMember ( mx . getUserId ( ) ) ? . powerLevel || 0 ;
const canChangeRole = (
room . currentState . maySendEvent ( 'm.room.power_levels' , mx . getUserId ( ) )
&& ( powerLevel < myPowerLevel || userId === mx . getUserId ( ) )
) ;
2022-04-25 20:21:21 +05:30
const handleChangePowerLevel = async ( newPowerLevel ) => {
2022-01-12 13:57:13 +05:30
if ( newPowerLevel === powerLevel ) return ;
2022-02-01 09:41:50 +05:30
const SHARED _POWER _MSG = 'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?' ;
const DEMOTING _MYSELF _MSG = 'You will not be able to undo this change as you are demoting yourself. Are you sure?' ;
const isSharedPower = newPowerLevel === myPowerLevel ;
const isDemotingMyself = userId === mx . getUserId ( ) ;
if ( isSharedPower || isDemotingMyself ) {
2022-04-25 20:21:21 +05:30
const isConfirmed = await confirmDialog (
'Change power level' ,
isSharedPower ? SHARED _POWER _MSG : DEMOTING _MYSELF _MSG ,
'Change' ,
'caution' ,
) ;
if ( ! isConfirmed ) return ;
roomActions . setPowerLevel ( roomId , userId , newPowerLevel ) ;
2022-02-01 09:41:50 +05:30
} else {
2022-01-12 13:57:13 +05:30
roomActions . setPowerLevel ( roomId , userId , newPowerLevel ) ;
}
} ;
2022-01-12 16:46:56 +05:30
2022-01-12 13:57:13 +05:30
const handlePowerSelector = ( e ) => {
openReusableContextMenu (
'bottom' ,
getEventCords ( e , '.btn-surface' ) ,
( closeMenu ) => (
< PowerLevelSelector
value = { powerLevel }
max = { myPowerLevel }
onSelect = { ( pl ) => {
closeMenu ( ) ;
handleChangePowerLevel ( pl ) ;
} }
/ >
) ,
) ;
} ;
2021-10-18 17:25:52 +02:00
return (
< div className = "profile-viewer" >
< div className = "profile-viewer__user" >
2022-01-12 16:46:56 +05:30
< Avatar imageSrc = { avatarUrl } text = { username } bgColor = { colorMXID ( userId ) } size = "large" / >
2021-10-18 17:25:52 +02:00
< div className = "profile-viewer__user__info" >
2021-12-16 17:55:16 +05:30
< Text variant = "s1" weight = "medium" > { twemojify ( username ) } < / Text >
2021-11-23 11:56:02 +05:30
< Text variant = "b2" > { twemojify ( userId ) } < / Text >
2021-10-18 17:25:52 +02:00
< / div >
< div className = "profile-viewer__user__role" >
< Text variant = "b3" > Role < / Text >
2022-01-12 13:57:13 +05:30
< Button
onClick = { canChangeRole ? handlePowerSelector : null }
iconSrc = { canChangeRole ? ChevronBottomIC : null }
>
2022-01-10 20:34:54 +05:30
{ ` ${ getPowerLabel ( powerLevel ) || 'Member' } - ${ powerLevel } ` }
< / Button >
2021-10-18 17:25:52 +02:00
< / div >
< / div >
2022-01-12 18:26:52 +05:30
< ModerationTools roomId = { roomId } userId = { userId } / >
2021-10-18 17:25:52 +02:00
< SessionInfo userId = { userId } / >
{ userId !== mx . getUserId ( ) && (
2022-01-12 18:26:52 +05:30
< ProfileFooter roomId = { roomId } userId = { userId } onRequestClose = { closeDialog } / >
2021-10-18 17:25:52 +02:00
) }
< / div >
) ;
2022-01-12 16:46:56 +05:30
} ;
2021-10-18 17:25:52 +02:00
return (
< Dialog
className = "profile-viewer__dialog"
isOpen = { isOpen }
2022-01-13 09:42:23 +05:30
title = { room ? . name ? ? '' }
2021-12-14 17:26:32 +05:30
onAfterClose = { handleAfterClose }
2022-01-12 16:46:56 +05:30
onRequestClose = { closeDialog }
contentOptions = { < IconButton src = { CrossIC } onClick = { closeDialog } tooltip = "Close" / > }
2021-10-18 17:25:52 +02:00
>
2021-12-14 17:26:32 +05:30
{ roomId ? renderProfile ( ) : < div / > }
2021-10-18 17:25:52 +02:00
< / Dialog >
) ;
}
export default ProfileViewer ;