Skip to content

Commit

Permalink
Merge pull request #922 from The-Commit-Company/dev-2
Browse files Browse the repository at this point in the history
merging dev in 131
janhvipatil authored May 17, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents fb31480 + 4bf846f commit b83a890
Showing 129 changed files with 2,853 additions and 1,647 deletions.
3 changes: 2 additions & 1 deletion mobile/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "mobile",
"private": true,
"version": "1.5.2",
"version": "1.5.5",
"type": "module",
"scripts": {
"dev": "vite",
@@ -37,6 +37,7 @@
"cal-sans": "^1.0.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"dayjs": "^1.11.10",
"emoji-picker-element": "^1.21.1",
"firebase": "^10.9.0",
"frappe-react-sdk": "^1.5.1",
136 changes: 92 additions & 44 deletions mobile/src/components/features/channels/ChannelList.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { BiGlobe, BiHash, BiLockAlt } from 'react-icons/bi'
import { ChannelListItem, UnreadCountData } from '@/utils/channel/ChannelListProvider'
import { useMemo } from 'react'
import { Link } from 'react-router-dom'
import { Badge, Heading, Text } from '@radix-ui/themes'
import { useContext, useMemo, useRef } from 'react'
import { Badge, Box, Text } from '@radix-ui/themes'
import { IonItem, IonItemOption, IonItemOptions, IonItemSliding, IonList } from '@ionic/react'
import { RiPushpinLine, RiUnpinLine } from 'react-icons/ri'
import useCurrentRavenUser from '@/hooks/useCurrentRavenUser'
import { FrappeConfig, FrappeContext } from 'frappe-react-sdk'
import { RavenUser } from '@/types/Raven/RavenUser'

interface ChannelListProps {
data: ChannelListItem[],
@@ -11,65 +15,109 @@ interface ChannelListProps {

export const ChannelList = ({ data, unread_count }: ChannelListProps) => {

const { channels, unreadChannels } = useMemo(() => {
if (!data) return { channels: [], unreadChannels: [] }
if (!unread_count) return { channels: data.map(d => ({ ...d, unreadCount: 0 })), unreadChannels: [] }
const { myProfile } = useCurrentRavenUser()

const c = data.map(channel => {
const unreadCountForChannel = unread_count.channels.find((unread) => unread.name == channel.name)?.unread_count
return { ...channel, unreadCount: unreadCountForChannel ?? 0 }
})
const { channels } = useMemo(() => {
if (!data) return { channels: [] }

const unreadChannels = c.filter(channel => channel.unreadCount > 0)
const pinnedChannelIDs = myProfile?.pinned_channels?.map(pin => pin.channel_id)

return { channels: c, unreadChannels }
}, [data, unread_count])
const channelList = []

return (
<div>
{unreadChannels?.length > 0 &&
<div className='pb-2'>
<Heading as='h4' className='px-4 py-2 not-cal bg-gray-2' weight='medium' size='2'>Unread</Heading>
<ul>
{unreadChannels?.map(channel => <ChannelItem
channel={channel}
unreadCount={channel.unreadCount}
key={channel.name} />)}
</ul>
</div>
}
for (const channel of data) {
if (channel.is_archived == 1) continue
const isPinned = pinnedChannelIDs?.includes(channel.name) ?? false
const unreadCountForChannel = unread_count?.channels.find((unread) => unread.name == channel.name)?.unread_count ?? 0

<div className='pb-2'>
{unreadChannels?.length > 0 &&
<Heading as='h4' className='px-4 py-2 not-cal bg-gray-2' weight='medium' size='2'>All</Heading>
}<ul>
{channels?.map(channel => <ChannelItem
channel={channel}
unreadCount={channel.unreadCount}
key={channel.name} />)}
</ul>
</div>
</div>
channelList.push({
...channel,
isPinned,
unreadCount: unreadCountForChannel
})
}

// Sort the channel list by pinned channels first
const pinnedChannels = channelList.filter(channel => channel.isPinned)
const unpinnedChannels = channelList.filter(channel => !channel.isPinned)

return { channels: [...pinnedChannels, ...unpinnedChannels] }

}, [data, unread_count, myProfile])

return (
<div className='pb-2'>
<IonList>
{channels?.map(channel => <ChannelItem
channel={channel}
unreadCount={channel.unreadCount}
key={channel.name} />)}
</IonList>
</div>
)
}

const ChannelItem = ({ channel, unreadCount }: { channel: ChannelListItem, unreadCount?: number }) => {
interface ChannelListItemWithPinned extends ChannelListItem {
isPinned: boolean
}

const ChannelItem = ({ channel, unreadCount }: { channel: ChannelListItemWithPinned, unreadCount?: number }) => {

const ref = useRef<HTMLIonItemSlidingElement>(null)

const close = () => {
ref.current?.close()
}

return (
<li key={channel.name} className="list-none">
<Link to={`/channel/${channel.name}`} className='block px-4 py-2.5 active:bg-accent active:rounded'>
<div className='flex justify-between items-center text-foreground'>
<IonItemSliding ref={ref}>
<IonItemOptions side='start'>
<PinAction channelID={channel.name} isPinned={channel.isPinned} onComplete={close} />
</IonItemOptions>
<IonItem
routerLink={`/channel/${channel.name}`}
detail={false}
// lines='none'
className='block'>
<div className='flex justify-between w-full items-center text-foreground'>
<div className='flex items-center space-x-2'>
<div>
{channel.type === "Private" ? <BiLockAlt size='20' /> : channel.type === "Public" ? <BiHash size='20' /> :
<BiGlobe size='20' />}
</div>
<Text size='3' weight='medium'>{channel.channel_name}</Text>
</div>
{unreadCount ? <Badge radius='large' size='2' variant='solid'>{unreadCount < 100 ? unreadCount : '99+'}</Badge> : null}
{unreadCount ? <Badge radius='large' size='2' variant='solid'>{unreadCount < 100 ? unreadCount : '99+'}</Badge> :
channel.isPinned ? <Box><RiPushpinLine size='16' className='text-gray-9' /></Box> : null
}
</div>
</Link>
</li>
</IonItem>


</IonItemSliding>

)
}

const PinAction = ({ channelID, isPinned, onComplete }: { channelID: string, isPinned: boolean, onComplete: VoidFunction }) => {

const { mutate } = useCurrentRavenUser()

const { call } = useContext(FrappeContext) as FrappeConfig

const onClick: React.MouseEventHandler<HTMLIonItemOptionElement> = (e) => {
call.post('raven.api.raven_channel.toggle_pinned_channel', {
channel_id: channelID
}).then((res: { message: RavenUser }) => {
if (res.message) {
mutate({ message: res.message }, { revalidate: false })
}
})
onComplete()
}
return <IonItemOption onClick={onClick}>
<div className='flex gap-2'>
{isPinned ? <RiUnpinLine size='20' /> : <RiPushpinLine size='20' />}
{isPinned ? 'Unpin' : 'Pin'}
</div>
</IonItemOption>
}
Original file line number Diff line number Diff line change
@@ -30,7 +30,9 @@ export type Member = {
user_image: string | undefined
first_name: string
is_admin?: boolean,
type?: 'User' | 'Bot'
allow_notifications?: 0 | 1,
type?: 'User' | 'Bot',
channel_member_name?: string
}

export type ChannelMembers = {
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useGetChannelMembers } from "@/hooks/useGetChannelMembers"
import { UserContext } from "@/utils/auth/UserProvider"
import { IonItem, IonToggle, useIonToast } from "@ionic/react"
import { useFrappeGetCall, useFrappePostCall } from "frappe-react-sdk"
import { useContext, useMemo, useState } from "react"
import { useParams } from "react-router-dom"

export const ChannelPushNotificationToggle = () => {

const { data } = useFrappeGetCall<{ message: boolean }>('raven.api.notification.are_push_notifications_enabled', undefined, undefined, {
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
})

if (data?.message) {
return (
<ToggleButton />
)
}

return null
}

const ToggleButton = () => {

const { channelID } = useParams<{ channelID: string }>()

const { currentUser } = useContext(UserContext)

const { channelMembers, mutate } = useGetChannelMembers(channelID)

const { call } = useFrappePostCall('raven.api.notification.toggle_push_notification_for_channel')

const { member, isNotificationEnabled } = useMemo(() => {
const member = channelMembers?.find(member => member.name === currentUser)
return {
member,
isNotificationEnabled: member?.allow_notifications
}
}, [channelMembers, currentUser])

const [loading, setLoading] = useState(false)

const [present] = useIonToast()

const onToggle = () => {
if (member && member.channel_member_name) {
setLoading(true)
call({
member: member.channel_member_name,
allow_notifications: isNotificationEnabled ? 0 : 1
})
.catch(() => {
present({
message: 'Failed to update notification settings',
duration: 1500,
color: 'danger',
position: 'bottom',
})
})
.then(() => mutate())
.finally(() => setLoading(false))

}
}

if (!member || !member.channel_member_name) {
return null
}



return <IonItem color='light'>
<IonToggle
onIonChange={onToggle}
disabled={loading}
checked={isNotificationEnabled ? true : false}>
Enable Push Notifications
</IonToggle>
</IonItem>
}
23 changes: 20 additions & 3 deletions mobile/src/components/features/chat-input/FileUploadModal.tsx
Original file line number Diff line number Diff line change
@@ -3,7 +3,10 @@ import { IonButton, IonButtons, IonContent, IonHeader, IonIcon, IonItem, IonLabe
import { addSharp, alertCircle, checkmarkCircle, closeCircleOutline } from 'ionicons/icons';
import { getFileExtension } from '../../../../../raven-app/src/utils/operations';
import { getFileExtensionIcon } from '../../../utils/layout/fileExtensions';
import { useFrappeCreateDoc, useFrappeFileUpload, useFrappeUpdateDoc } from 'frappe-react-sdk';
import { useFrappeFileUpload } from 'frappe-react-sdk';
import { useGetUser } from '@/hooks/useGetUser';
import { useGetChannelData } from '@/hooks/useGetChannelData';
import { DMChannelListItem } from '@/utils/channel/ChannelListProvider';

type Props = {
files: File[],
@@ -21,7 +24,21 @@ export const FileUploadModal = ({ files, setFiles, pickFiles, channelID, onMessa

const [loading, setLoading] = useState(false)
const [errorMap, setErrorMap] = React.useState<{ [key: string]: string }>({})

const { channel } = useGetChannelData(channelID)

const generateDescriptionMessage = (channel: DMChannelListItem) =>{
let text = "Files will be shared with "
if(channel.is_direct_message===1 && channel.is_self_message===0){
const user = useGetUser(channel.peer_user_id)
return text + user?.full_name || user?.first_name
}else if(channel.is_direct_message===1 && channel.is_self_message===1){
// ** should the line be changed?
return "Files will be stored here for your reference"
} else{
return text + "all channel members"
}
}


const uploadFiles = async () => {
setLoading(true)
@@ -81,7 +98,7 @@ export const FileUploadModal = ({ files, setFiles, pickFiles, channelID, onMessa
</IonHeader>
<IonContent>
<div className='py-2 mt-2 px-4 text-sm'>
<IonText color='medium'>Files will be shared with all channel members.</IonText>
<IonText color='medium'>{generateDescriptionMessage(channel as DMChannelListItem)}.</IonText>
</div>
<IonList>
{files.map((f, i) => <IonItem key={f.name + i}>
4 changes: 2 additions & 2 deletions mobile/src/components/features/chat-input/tiptap.styles.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.tiptap p.is-editor-empty:first-child::before {
.tiptap-editor p.is-editor-empty:first-child::before {
color: var(--gray-8);
content: attr(data-placeholder);
float: left;
@@ -9,7 +9,7 @@
pointer-events: none;
}

.tiptap.ProseMirror {
.tiptap-editor.ProseMirror {
max-height: 240px;
overflow: auto;
}
16 changes: 5 additions & 11 deletions mobile/src/components/features/chat-space/ChatInterface.tsx
Original file line number Diff line number Diff line change
@@ -76,7 +76,7 @@ export const ChatInterface = ({ channel }: { channel: ChannelListItem | DMChanne
}
});

const { data: channelMembers } = useFrappeGetCall<{ message: ChannelMembersMap }>('raven.api.chat.get_channel_members', {
const { data: channelMembers, isLoading: channelMembersLoading } = useFrappeGetCall<{ message: ChannelMembersMap }>('raven.api.chat.get_channel_members', {
channel_id: channel.name
}, `raven.api.chat.get_channel_members.${channel.name}`, {
revalidateOnFocus: false,
@@ -123,15 +123,9 @@ export const ChatInterface = ({ channel }: { channel: ChannelListItem | DMChanne
</div>
<div className='flex items-center justify-between gap-2 w-full'>
<div className='grow p-1'>
{
isOpenChannel ?

<ChatHeader channel={channel} /> :
<Link to={`${channel.name}/channel-settings`}>
<ChatHeader channel={channel} />
</Link>

}
<Link to={`${channel.name}/channel-settings`}>
<ChatHeader channel={channel} />
</Link>
</div>
{/* TO-DO: Add Other optional buttons here later */}
{/* <div hidden aria-hidden>
@@ -212,7 +206,7 @@ export const ChatInterface = ({ channel }: { channel: ChannelListItem | DMChanne
hidden={!!error}
className='block relative z-10 order-1 w-full'
>
{channel && channel.is_archived === 0 && !isDM && !isUserInChannel && channel.type !== 'Open' ?
{channel && !channelMembersLoading && channel.is_archived === 0 && !isDM && !isUserInChannel && channel.type !== 'Open' ?
<JoinChannelButton channelData={channel} /> :

<div
Original file line number Diff line number Diff line change
@@ -2,8 +2,8 @@ import { Message, PollMessage } from '../../../../../../types/Messaging/Message'
import { UserFields } from '@/utils/users/UserListProvider'
import { MessageSenderAvatar } from '../chat-view/MessageBlock'
import { IonText } from '@ionic/react'
import { DateObjectToTimeString } from '@/utils/operations/operations'
import parse from 'html-react-parser';
import { toHourMinuteAmPm } from '@/utils/operations/dateConversions';

type MessagePreview = { message: Message, user?: UserFields }

@@ -16,7 +16,7 @@ const MessagePreview = ({ message, user }: MessagePreview) => {
<div className='overflow-x-clip'>
<div className='flex items-end pb-1.5'>
<IonText className='font-bold text-zinc-50 text-sm'>{user?.full_name ?? message.owner}</IonText>
<IonText className='text-xs pl-1.5 text-zinc-500'>{DateObjectToTimeString(message.creation)}</IonText>
<IonText className='text-xs pl-1.5 text-zinc-500'>{toHourMinuteAmPm(message.creation)}</IonText>
</div>
{message.message_type === 'Text' && <div className='text-base line-clamp-3 text-ellipsis'>{parse(message.content ?? '')}</div>}
{message.message_type === 'Image' && <div className='flex items-center space-x-2'>
Loading

0 comments on commit b83a890

Please sign in to comment.