feat: init

This commit is contained in:
montaghanmy
2023-03-23 11:03:16 +01:00
commit 10fe6f78d1
11518 changed files with 509786 additions and 0 deletions
@@ -0,0 +1,107 @@
import Api from '../../global/framework/api-service';
import { ChannelType } from 'app/features/channels/types/channel';
import { TwakeService } from '../../global/framework/registry-decorator-service';
import { delayRequest } from 'app/features/global/utils/managedSearchRequest';
import { removeBadgesNow } from 'app/features/users/hooks/use-notifications';
import Workspace from 'app/deprecated/workspaces/workspaces';
import Logger from 'features/global/framework/logger-service';
const PREFIX = '/internal/services/channels/v1/companies';
export type SearchOptions = {
company_id: string;
page_token?: string;
limit?: number;
};
@TwakeService('ChannelAPIClientService')
class ChannelAPIClientService {
private logger = Logger.getLogger('ChannelAPIClientService');
async getDirect(companyId: string, membersId: string[]) {
return Api.post<{ options: { members: string[] }; resource: unknown }, { resource: ChannelType }>(
`${PREFIX}/${companyId}/workspaces/direct/channels`,
{ options: { members: membersId }, resource: {} },
).then(result => result.resource);
}
async get(companyId: string, workspaceId: string, channelId: string): Promise<ChannelType> {
return Api.get<{ resource: ChannelType }>(
`${PREFIX}/${companyId}/workspaces/${workspaceId}/channels/${channelId}`,
).then(result => result?.resource);
}
async save(
channel: ChannelType,
context: { companyId: string; workspaceId: string; channelId?: string },
) {
return Api.post<{ resource: ChannelType }, { resource: ChannelType }>(
`${PREFIX}/${context.companyId}/workspaces/${context.workspaceId}/channels${
context?.channelId ? `/${context.channelId}` : ''
}`,
{
resource: channel,
},
).then(result => result.resource);
}
async read(
companyId: string,
workspaceId: string,
channelId: string,
{ status = true, requireFocus = false, now = false },
): Promise<void> {
if (requireFocus && !document.hasFocus()) return;
if (status) removeBadgesNow('channel', channelId);
delayRequest(
'reach-end-read-channel-' + channelId,
async () =>
await Api.post<{ value: boolean }, void>(
`${PREFIX}/${companyId}/workspaces/${workspaceId}/channels/${channelId}/read`,
{
value: status,
},
),
{ doInitialCall: now, timeout: 2000 },
);
}
async recent(companyId: string, limit: number): Promise<ChannelType[]> {
try {
const res = await Api.get<{ resources: ChannelType[] }>(
`${PREFIX}/${companyId}/channels/recent?limit=${limit}`,
);
return res.resources;
} catch (e) {
console.error("Can't retrieve channels", e);
return [];
}
}
async search(
searchString: string | null,
options: SearchOptions,
): Promise<{ resources: ChannelType[] }> {
if (!searchString) {
return { resources: await this.recent(options.company_id, options?.limit || 100) };
}
const companyId = options?.company_id || Workspace.currentGroupId;
const query = `/internal/services/channels/v1/companies/${companyId}/search?q=${searchString}`;
const res = await Api.getWithParams<{ resources: ChannelType[] }>(query, options);
this.logger.debug(
`Search by name "${searchString}" with options`,
options,
'. Found',
res.resources.length,
'channels',
);
return res;
}
}
const ChannelAPIClient = new ChannelAPIClientService();
export default ChannelAPIClient;
@@ -0,0 +1,98 @@
import Api from '../../global/framework/api-service';
import { ChannelType } from 'app/features/channels/types/channel';
import { TwakeService } from '../../global/framework/registry-decorator-service';
import { WebsocketRoom } from '../../global/types/websocket-types';
type ChannelsMineGetResponse = { resources: ChannelType[]; websockets: WebsocketRoom[] };
type ChannelsMineDeleteBaseResponse = {
statusCode: number;
error: string;
message: string;
};
@TwakeService('ChannelsMineAPIClientService')
class ChannelsMineAPIClient {
private readonly prefix = '/internal/services/channels/v1/companies';
private readonly realtime: Map<string, WebsocketRoom[]> = new Map();
websockets(companyId: string, workspaceId: string): WebsocketRoom[] {
return this.realtime.get(this.getRealtimeKey(companyId, workspaceId)) || [];
}
getRealtimeKey(companyId: string, workspaceId: string) {
return `/companies/${companyId}/workspaces/${workspaceId}/channels`;
}
async save(
channel: ChannelType,
context: { companyId: string; workspaceId: string; channelId?: string },
) {
return Api.post<{ resource: ChannelType }, { resource: ChannelType }>(
`${this.prefix}/${context.companyId}/workspaces/${context.workspaceId}/channels${
context?.channelId ? `/${context.channelId}` : ''
}`,
{
resource: channel,
},
).then(result => result.resource);
}
/**
* @param companyId
* @param workspaceId
* @return channels that user is already a member
*/
async get(context: { companyId: string; workspaceId?: string }): Promise<ChannelType[]> {
context.workspaceId = context.workspaceId || 'direct';
return Api.get<ChannelsMineGetResponse>(
`${this.prefix}/${context.companyId}/workspaces/${
context.workspaceId
}/channels?mine=1&websockets=1${context.workspaceId == 'direct' ? '&include_users=1' : ''}`,
).then(result => {
this.realtime.set(
this.getRealtimeKey(context.companyId, context.workspaceId as string),
result.websockets,
);
return result.resources;
});
}
/**
* Remove user from a channel.
* Every user in the channel (except guests) can remove an user.
* A system message will be sent.
* We cannot call this route for direct channels.
* @param userId string
* @param context companyId - workspaceId - channelId
*/
async removeUser(
userId: string,
context: { companyId: string; workspaceId: string; channelId: string },
): Promise<ChannelsMineDeleteBaseResponse> {
return Api.delete<ChannelsMineDeleteBaseResponse>(
`${this.prefix}/${context.companyId}/workspaces/${context.workspaceId}/channels/${context.channelId}/members/${userId}`,
);
}
/**
* Remove a channel, this action is not reversible.
* Direct channels can not be removed.
* Only administrators and channel owner can remove a channel.
* @param companyId string
* @param workspaceId string
* @param channelId string
*
*/
removeChannel(
companyId: string,
workspaceId: string,
channelId: string,
): Promise<ChannelsMineDeleteBaseResponse> {
return Api.delete<ChannelsMineDeleteBaseResponse>(
`${this.prefix}/${companyId}/workspaces/${workspaceId}/channels/${channelId}`,
);
}
}
export default new ChannelsMineAPIClient();
@@ -0,0 +1,52 @@
import {
ChannelsReachableGetResponse,
ChannelsReachableInviteUserRequest,
ChannelsReachableInviteUserResponse,
} from 'app/features/channels/types/channels-reachable-types';
import Api from '../../global/framework/api-service';
import { ChannelType } from 'app/features/channels/types/channel';
import { TwakeService } from '../../global/framework/registry-decorator-service';
import { ChannelMemberType } from 'app/features/channel-members/types/channel-member-types';
@TwakeService('ChannelsReachableAPIClientService')
class ChannelsReachableAPIClientService {
private readonly prefix = '/internal/services/channels/v1/companies';
/**
* @param companyId
* @param workspaceId
* @return channels that user is not a member but could join
*/
async get(companyId: string, workspaceId: string): Promise<ChannelType[]> {
return Api.get<ChannelsReachableGetResponse>(
`${this.prefix}/${companyId}/workspaces/${workspaceId}/channels`,
).then(result => result.resources);
}
/**
* Add user to a channel.
* Every user in the channel (except guests) can invite or remove someone.
* A system message will be sent on invitations.
* @param companyId string
* @param workspaceId string
* @param userId string
*
*/
async inviteUser(
companyId: string,
workspaceId: string,
channelId: string,
userId: string,
): Promise<ChannelMemberType> {
return Api.post<ChannelsReachableInviteUserRequest, ChannelsReachableInviteUserResponse>(
`${this.prefix}/${companyId}/workspaces/${workspaceId}/channels/${channelId}/members`,
{
resource: {
user_id: userId,
},
},
).then(result => result.resource);
}
}
const ChannelsReachableAPIClient = new ChannelsReachableAPIClientService();
export default ChannelsReachableAPIClient;
@@ -0,0 +1,46 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect } from 'react';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import useRouterChannel from 'app/features/router/hooks/use-router-channel';
import RouterServices from 'app/features/router/services/router-service';
import { useDirectChannels } from './use-direct-channels';
import { usePublicOrPrivateChannels } from './use-public-or-private-channels';
import LocalStorage from 'app/features/global/framework/local-storage-service';
const fromLocalStorage = LocalStorage.getItem('default_channel_id_per_workspace');
const activeChannelPerWorkspace: { [key: string]: string } =
(typeof fromLocalStorage === 'object' ? (fromLocalStorage as any) : {}) || {};
export function useAutoSelectChannel() {
const workspaceId = useRouterWorkspace();
const channelId = useRouterChannel();
const { directChannels } = useDirectChannels();
const { publicChannels, privateChannels } = usePublicOrPrivateChannels();
const channels = [...directChannels, ...publicChannels, ...privateChannels];
useEffect(() => {
if (channelId) {
activeChannelPerWorkspace[workspaceId] = channelId;
LocalStorage.setItem('default_channel_id_per_workspace', activeChannelPerWorkspace);
}
}, [channelId]);
useEffect(() => {
if (!channelId && channels.length > 0) {
let preferedChannelId = activeChannelPerWorkspace[workspaceId];
if (!preferedChannelId || !channels.find(c => c.id === preferedChannelId)) {
preferedChannelId =
channels.sort((a, b) => (b.last_activity || 0) - (a.last_activity || 0))[0]?.id ||
preferedChannelId;
}
if (preferedChannelId) {
const url = RouterServices.generateRouteFromState({
channelId: preferedChannelId,
});
RouterServices.replace(url);
}
}
}, [workspaceId, channels.length > 0]);
}
@@ -0,0 +1,111 @@
import { useRecoilState } from 'recoil';
import useRouterChannel from 'app/features/router/hooks/use-router-channel';
import useRouterCompany from 'app/features/router/hooks/use-router-company';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import { LoadingState } from 'app/features/global/state/atoms/Loading';
import messageApiClient from 'app/features/messages/api/message-api-client';
import { useGlobalEffect } from 'app/features/global/hooks/use-global-effect';
import {
channelAttachmentFileState,
channelAttachmentListState,
channelAttachmentMediaState,
} from '../state/channel-attachment-list';
import _ from 'lodash';
export const useChannelAttachmentList = (type: 'file' | 'media') => {
const limit = 25;
const companyId = useRouterCompany();
const workspaceId = useRouterWorkspace();
const channelId = useRouterChannel();
const [loading, setLoading] = useRecoilState(
LoadingState(`useChannelAttachmentList-${type}-${companyId}-${workspaceId}-${channelId}`),
);
const [isOpen] = useRecoilState(channelAttachmentListState);
const [, setchannelFiles] = useRecoilState(channelAttachmentFileState(companyId));
const [, setchannelMedia] = useRecoilState(channelAttachmentMediaState(companyId));
const [result, setResult] = useRecoilState(
type === 'media'
? channelAttachmentMediaState(companyId)
: channelAttachmentFileState(companyId),
);
const options = _.omitBy(
{
limit,
is_file: type === 'file' || undefined,
is_media: type === 'media' || undefined,
workspace_id: workspaceId,
channel_id: channelId,
},
_.isUndefined,
);
const loadItems = async () => {
setLoading(true);
const response = await messageApiClient.searchFile(null, options);
const results = (response.resources || []).sort(
(a, b) => (b?.message?.created_at || 0) - (a?.message?.created_at || 0),
);
const update = {
results,
nextPage: response.next_page_token || null,
};
setResult(update);
setLoading(false);
};
const loadMore = async () => {
if (result.nextPage && result.results.length % limit === 0) {
const response = await messageApiClient.searchFile(null, { ...options, next_page_token: result.nextPage });
const results = (response.resources || []).sort(
(a, b) => (b?.message?.created_at || 0) - (a?.message?.created_at || 0),
);
const update = {
results: _.uniqBy([...result.results, ...results] || [], 'id'),
nextPage: response.next_page_token || null,
};
setResult(update);
}
}
const reset = () => {
const update = {
results: [],
nextPage: null,
}
setchannelFiles(update);
setchannelMedia(update);
}
useGlobalEffect(
`useChannelAttachmentList${type}`,
() => {
if (!isOpen) {
reset();
}
},
[channelId, workspaceId, isOpen],
);
return {
loading,
result: result.results,
loadItems,
loadMore,
};
};
export const useChannelMediaList = () => {
return useChannelAttachmentList('media');
};
export const useChannelFileList = () => {
return useChannelAttachmentList('file');
};
@@ -0,0 +1,150 @@
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useRealtimeRoom } from 'app/features/global/hooks/use-realtime';
import WorkspaceAPIClient from 'app/features/workspaces/api/workspace-api-client';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import {
ChannelWritingActivityState,
ChannelWritingActivityType,
} from '../state/channel-writing-activity';
import { ThreadWritingActivitySelector } from '../../messages/state/selectors/thread-writing-activity';
import UserService from 'app/features/users/services/current-user-service';
import useRouterCompany from '../../router/hooks/use-router-company';
import { useCallback, useRef } from 'react';
import { useCurrentUser } from '../../users/hooks/use-current-user';
const MAX_DELAY_BETWEEN_KEYDOWN = 500;
const MIN_DELAY_BETWEEN_EMIT = 8000;
const MAX_DELAY_AFTER_LAST_WRITE_EVENT = 10000;
export type WritingEvent = {
type: 'writing';
event: {
channel_id: string;
thread_id: string;
user_id: string;
name: string;
is_writing: boolean;
};
};
export type ChannelWritingActivityTypeEmit = {
iAmWriting: (writing: boolean) => void;
};
export function useChannelWritingActivityState(
channelId: string,
threadId?: string | null,
): ChannelWritingActivityType[] {
const threadIdSelector = useRecoilValue(
ThreadWritingActivitySelector({ channelId: channelId, threadId: threadId || '' }),
);
return threadIdSelector;
}
const receivedWritingTimeout = new Map<string, number>();
export default function useChannelWritingActivity() {
const companyId = useRouterCompany();
const { user } = useCurrentUser();
const setChannelWritingActivityState = useRecoilCallback(
({ set, snapshot }) =>
async (event: WritingEvent['event']) => {
let currentList: ChannelWritingActivityType[] = await snapshot.getPromise(
ChannelWritingActivityState(event.channel_id),
);
const newEvent: ChannelWritingActivityType = {
threadId: event.thread_id,
userId: event.user_id,
name: event.name,
};
currentList = currentList.filter(elem => elem.userId !== newEvent.userId);
if (event.is_writing) {
currentList = [...currentList, newEvent];
}
currentList = currentList.filter(elem => elem.userId !== user?.id);
set(ChannelWritingActivityState(event.channel_id), currentList);
//Fallback stop is_writing in case of lost connection
if (receivedWritingTimeout.has(event.user_id))
clearTimeout(receivedWritingTimeout.get(event.user_id));
if (event.is_writing) {
receivedWritingTimeout.set(
event.user_id,
window.setTimeout(() => {
setChannelWritingActivityState({ ...event, is_writing: false });
}, MAX_DELAY_AFTER_LAST_WRITE_EVENT),
);
}
},
);
useRealtimeRoom<WritingEvent>(
WorkspaceAPIClient.websockets(companyId)[0],
'useChannelWritingActivity',
(action, resource) => {
if (action === 'event' && resource.type === 'writing') {
setChannelWritingActivityState(resource.event);
}
},
);
}
export function useChannelWritingActivityEmit(
channelId: string,
threadId: string | null,
): ChannelWritingActivityTypeEmit {
const companyId = useRouterCompany();
const { user } = useCurrentUser();
const { send } = useRealtimeRoom<WritingEvent>(
WorkspaceAPIClient.websockets(companyId)[0],
'useChannelWritingActivityEmit',
() => undefined,
);
(window as any).send = send;
const iAmWriting = useCallback(
async (writing: boolean) => {
if (user)
send({
type: 'writing',
event: {
channel_id: channelId,
thread_id: threadId,
user_id: user.id,
name: UserService.getFullName(user),
is_writing: writing,
},
} as WritingEvent);
},
[send],
);
return { iAmWriting };
}
/** Keyboard typeing detection helper */
let writeTimeout = setTimeout(() => {}, 0);
export const useWritingDetector = () => {
const lastEmit = useRef(new Date().getTime());
const onKeydown = useCallback((emit: (value: boolean) => unknown) => {
const now = new Date().getTime();
if (now - lastEmit.current > MIN_DELAY_BETWEEN_EMIT) {
lastEmit.current = now;
emit(true);
}
if (writeTimeout) {
clearTimeout(writeTimeout);
}
writeTimeout = setTimeout(() => {
emit(false);
lastEmit.current = 0;
}, MAX_DELAY_BETWEEN_KEYDOWN);
}, []);
return { onKeydown };
};
@@ -0,0 +1,89 @@
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { ChannelType } from 'app/features/channels/types/channel';
import { ChannelsState, ChannelSelector } from '../state/channels';
import ChannelAPIClient from '../api/channel-api-client';
import { useGlobalEffect } from 'app/features/global/hooks/use-global-effect';
import useRouterCompany from 'app/features/router/hooks/use-router-company';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import { LoadingState } from 'app/features/global/state/atoms/Loading';
//Keep the channels in a easy to use variable
let channelsKeeper: ChannelType[] = [];
export function useChannel(
channelId: string,
options?: { companyId: string; workspaceId: string },
) {
const companyId = options?.companyId || useRouterCompany();
const workspaceId = options?.workspaceId || useRouterWorkspace();
const hookId = 'useChannel-' + companyId + '-' + workspaceId + '-' + channelId;
const [loading, setLoading] = useRecoilState(LoadingState(hookId));
const channel = useRecoilValue(ChannelSelector(channelId)) as ChannelType;
const { set } = useSetChannel();
const save = async (channel: ChannelType) => {
setLoading(true);
await ChannelAPIClient.save(channel, {
companyId: channel.company_id || '',
workspaceId: channel.workspace_id || '',
channelId: channel.id,
});
set(channel);
setLoading(false);
};
const refresh = async () => {
setLoading(true);
const ch = await ChannelAPIClient.get(companyId, workspaceId, channelId);
if (ch && ch?.id) {
set(ch);
} else {
set({
id: channelId,
name: '',
visibility: 'private',
});
}
setLoading(false);
};
useGlobalEffect(
hookId,
async () => {
if (!channel) refresh();
},
[],
);
return { channel, save, loading, refresh };
}
export const useIsChannelMember = (channelId: string) => {
return !!useChannel(channelId)?.channel?.user_member?.user_id;
};
export const useIsReadOnlyChannel = (channelId: string) => {
return useChannel(channelId)?.channel?.is_readonly;
};
export function getChannel(channelId: string) {
return channelsKeeper.find(ch => ch.id === channelId);
}
export function getAllChannelsCache() {
return channelsKeeper;
}
export function useSetChannel() {
const set = useRecoilCallback(({ set }) => (channel: ChannelType) => {
if (channel.id) {
channelsKeeper = channelsKeeper.filter(c => c.id !== channel.id);
channelsKeeper.push(channel);
set(ChannelsState, channelsKeeper);
}
});
return { set };
}
@@ -0,0 +1,18 @@
import { useRecoilValue } from 'recoil';
import { LoadingState } from 'app/features/global/state/atoms/Loading';
export const useChannelsBarLoader = ({
companyId,
workspaceId,
}: {
companyId: string;
workspaceId: string;
}) => {
const publicOrPrivateContext = useRecoilValue(
LoadingState(`channels-${companyId}-${workspaceId}`),
);
const applicationContext = useRecoilValue(LoadingState(`applications-${companyId}`));
const directContext = useRecoilValue(LoadingState(`channels-direct-${companyId}`));
return { loading: publicOrPrivateContext || applicationContext || directContext };
};
@@ -0,0 +1,107 @@
import { useRecoilState, useSetRecoilState } from 'recoil';
import { ChannelType } from 'app/features/channels/types/channel';
import { DirectChannelsState } from '../state/channels';
import useRouterCompany from 'app/features/router/hooks/use-router-company';
import ChannelsMineAPIClient from 'app/features/channels/api/channels-mine-api-client';
import { useRealtimeRoom } from 'app/features/global/hooks/use-realtime';
import { LoadingState } from 'app/features/global/state/atoms/Loading';
import { useGlobalEffect } from 'app/features/global/hooks/use-global-effect';
import { useSetUserList } from 'app/features/users/hooks/use-user-list';
import { UserType } from 'app/features/users/types/user';
import { getChannel, useSetChannel } from './use-channel';
import ChannelAPIClient from '../api/channel-api-client';
import MenusManager from 'app/components/menus/menus-manager.jsx';
import RouterService from 'app/features/router/services/router-service';
export function useRefreshDirectChannels(): {
refresh: () => Promise<void>;
} {
const companyId = useRouterCompany();
const { set: setUserList } = useSetUserList('useRefreshDirectChannels');
const { set } = useSetChannel();
const _setDirectChannels = useSetRecoilState(DirectChannelsState(companyId));
const refresh = async () => {
const directChannels = await ChannelsMineAPIClient.get({ companyId, workspaceId: 'direct' });
if (directChannels) _setDirectChannels(directChannels);
const users: UserType[] = [];
directChannels.forEach(c => {
if (c.users) users.push(...c.users);
});
directChannels.forEach(c => set(c));
if (users) setUserList(users);
};
return { refresh };
}
export function useDirectChannelsSetup() {
const companyId = useRouterCompany();
const [, setLoading] = useRecoilState(LoadingState(`channels-direct-${companyId}`));
const [didLoad, setDidLoad] = useRecoilState(
LoadingState(`channels-direct-did-load-${companyId}`),
);
const { refresh } = useRefreshDirectChannels();
const { set } = useSetChannel();
useGlobalEffect(
'useDirectChannels',
async () => {
if (!didLoad) setLoading(true);
await refresh();
setLoading(false);
setDidLoad(true);
},
[companyId],
);
useRealtimeRoom<ChannelType & { _type: string }>(
ChannelsMineAPIClient.websockets(companyId, 'direct')[0],
'useDirectChannels',
(_action, event) => {
//TODO replace this to avoid calling backend every time
if (_action === 'saved') refresh();
if (_action === 'updated' && event._type === 'channel_activity') {
if (event.id)
set({ ...getChannel(event.id), stats: event.stats, last_message: event.last_message });
}
},
);
}
export function useDirectChannels(): {
directChannels: ChannelType[];
refresh: () => Promise<void>;
openDiscussion: (membersId: string[]) => Promise<void>;
} {
const companyId = useRouterCompany();
const [directChannels] = useRecoilState(DirectChannelsState(companyId));
const { refresh } = useRefreshDirectChannels();
const openDiscussion = async (membersIds: string[]) => {
const channel = await ChannelAPIClient.getDirect(companyId, membersIds);
await refresh();
if (channel) {
RouterService.push(
RouterService.generateRouteFromState({
channelId: channel.id,
companyId: channel.company_id,
}),
);
}
MenusManager.closeMenu();
};
return {
refresh,
directChannels,
openDiscussion,
};
}
@@ -0,0 +1,37 @@
import { ChannelType } from 'app/features/channels/types/channel';
import { useDirectChannels, useRefreshDirectChannels } from './use-direct-channels';
import {
usePublicOrPrivateChannels,
useRefreshPublicOrPrivateChannels,
} from './use-public-or-private-channels';
export function useRefreshFavoriteChannels(): {
refresh: () => Promise<void>;
} {
const { refresh: refreshPublicOrPrivateChannels } = useRefreshPublicOrPrivateChannels();
const { refresh: refreshDirectChannels } = useRefreshDirectChannels();
const refresh = async () => {
await refreshPublicOrPrivateChannels();
await refreshDirectChannels();
};
return { refresh };
}
export function useFavoriteChannels(): {
favoriteChannels: ChannelType[];
refresh: () => Promise<void>;
} {
const { publicChannels, privateChannels } = usePublicOrPrivateChannels();
const { directChannels } = useDirectChannels();
const { refresh } = useRefreshFavoriteChannels();
return {
favoriteChannels: [...publicChannels, ...privateChannels, ...directChannels].filter(
c => c.user_member?.favorite,
),
refresh,
};
}
@@ -0,0 +1,103 @@
import { useRecoilState, useSetRecoilState } from 'recoil';
import { ChannelType } from 'app/features/channels/types/channel';
import { MineChannelsState } from '../state/channels';
import useRouterCompany from 'app/features/router/hooks/use-router-company';
import ChannelsMineAPIClient from 'app/features/channels/api/channels-mine-api-client';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import { isPrivateChannel, isPublicChannel } from 'app/features/channels/utils/utils';
import { useRealtimeRoom } from 'app/features/global/hooks/use-realtime';
import { LoadingState } from 'app/features/global/state/atoms/Loading';
import { useGlobalEffect } from 'app/features/global/hooks/use-global-effect';
import { getChannel, useSetChannel } from './use-channel';
export function useRefreshPublicOrPrivateChannels(): {
refresh: () => Promise<void>;
} {
const companyId = useRouterCompany();
const workspaceId = useRouterWorkspace();
const _setMineChannels = useSetRecoilState(MineChannelsState({ companyId, workspaceId }));
const { set } = useSetChannel();
const refresh = async () => {
const res = await ChannelsMineAPIClient.get({ companyId, workspaceId });
res.forEach(c => set(c));
if (res) _setMineChannels(res);
};
return { refresh };
}
export function usePublicOrPrivateChannelsSetup() {
const companyId = useRouterCompany();
const workspaceId = useRouterWorkspace();
const [, setLoading] = useRecoilState(LoadingState(`channels-${companyId}-${workspaceId}`));
const [didLoad, setDidLoad] = useRecoilState(
LoadingState(`channels-did-load-${companyId}-${workspaceId}`),
);
const { refresh } = useRefreshPublicOrPrivateChannels();
const { set } = useSetChannel();
useGlobalEffect(
'usePublicOrPrivateChannels',
async () => {
if (!didLoad) setLoading(true);
await refresh();
setLoading(false);
setDidLoad(true);
},
[companyId, workspaceId],
);
//Public channels
useRealtimeRoom<ChannelType & { _type: string }>(
ChannelsMineAPIClient.websockets(companyId, workspaceId)[0],
'usePublicOrPrivateChannelsPublic',
(_action, event) => {
//TODO replace this to avoid calling backend every time
if (_action === 'saved') refresh();
if (_action === 'updated' && event._type === 'channel_activity') {
if (event.id)
set({ ...getChannel(event.id), stats: event.stats, last_message: event.last_message });
}
},
);
//Private channels
useRealtimeRoom<ChannelType & { _type: string }>(
ChannelsMineAPIClient.websockets(companyId, workspaceId)[1],
'usePublicOrPrivateChannelsPrivate',
(_action, event) => {
//TODO replace this to avoid calling backend every time
if (_action === 'saved') refresh();
if (_action === 'updated' && event._type === 'channel_activity') {
if (event.id)
set({ ...getChannel(event.id), stats: event.stats, last_message: event.last_message });
}
},
);
}
export function usePublicOrPrivateChannels(): {
privateChannels: ChannelType[];
publicChannels: ChannelType[];
refresh: () => Promise<void>;
} {
const companyId = useRouterCompany();
const workspaceId = useRouterWorkspace();
const [mineChannels] = useRecoilState(
MineChannelsState({ companyId, workspaceId }),
);
const { refresh } = useRefreshPublicOrPrivateChannels();
return {
refresh: refresh,
privateChannels: (mineChannels || [])?.filter(
c => c.visibility && isPrivateChannel(c.visibility),
),
publicChannels: (mineChannels || [])?.filter(
c => c.visibility && isPublicChannel(c.visibility),
),
};
}
@@ -0,0 +1,42 @@
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import { ChannelType } from 'app/features/channels/types/channel';
import { ReachableChannelsState } from '../state/channels';
import useRouterCompany from 'app/features/router/hooks/use-router-company';
import useRouterWorkspace from 'app/features/router/hooks/use-router-workspace';
import ChannelsReachableAPIClient from 'app/features/channels/api/channels-reachable-api-client';
import { useSetChannel } from './use-channel';
export function useReachableChannels(): {
reachableChannels: ChannelType[];
refresh: () => void;
} {
const companyId = useRouterCompany();
const workspaceId = useRouterWorkspace();
const [reachableChannels, _setReachableChannels] = useRecoilState(
ReachableChannelsState({ companyId, workspaceId }),
);
const { set } = useSetChannel();
const refresh = async () => {
const channels = await ChannelsReachableAPIClient.get(companyId, workspaceId);
channels.forEach(channel => {
set(channel);
});
if (channels) _setReachableChannels(channels);
};
useEffect(() => {
companyId.length > 1 && workspaceId.length > 1 && refresh();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [companyId, workspaceId]);
return {
refresh,
reachableChannels,
};
}
@@ -0,0 +1,44 @@
import Observable from 'app/deprecated/Observable/Observable';
import LocalStorage from 'app/features/global/framework/local-storage-service';
class ChannelsBarService extends Observable {
private callbacks = new Map<string, (state: boolean) => void>();
private ready = new Map<string, boolean>();
collectionIsReady(companyId: string, workspaceId: string, suffix?: string[]): void {
const callbackId = this.getCallbackId(companyId, workspaceId, suffix);
if (!this.ready.has(callbackId)) {
this.ready.set(callbackId, true);
this.notify();
}
}
isReady(companyId = '', workspaceId = '', suffix?: string[]): boolean {
return !!this.ready.get(this.getCallbackId(companyId, workspaceId, suffix));
}
updateCurrentChannelId(
companyId = '',
workspaceId = '',
channelId = '',
): void {
LocalStorage.setItem(this.getLocalStorageKey(companyId, workspaceId), channelId);
}
private getLocalStorageKey(companyId: string, workspaceId: string): string {
return `${companyId}:${workspaceId}:channel`;
}
private getCallbackId(companyId: string, workspaceId: string, suffix?: string[]): string {
let key = `${companyId}+${workspaceId}`;
if (suffix && suffix.length) {
key = [key, ...suffix].join('+');
}
return key;
}
}
export default new ChannelsBarService();
@@ -0,0 +1,28 @@
import { MessageFileType } from 'app/features/messages/types/message';
import { atom, atomFamily } from 'recoil';
export type ChannelAttachment = {
results: MessageFileType[];
nextPage: string | null;
};
export const activeChannelAttachementListTabState = atom<number>({
key: 'activeChannelAttachementListTabState',
default: 0,
});
export const channelAttachmentListState = atom<boolean>({
key: 'channelAttachmentListState',
default: false,
});
export const channelAttachmentMediaState = atomFamily<ChannelAttachment, string>({
key: 'channelAttachmentMediaState',
default: () => ({ results: [], nextPage: '' }),
});
export const channelAttachmentFileState = atomFamily<ChannelAttachment, string>({
key: 'channelAttachmentFileState',
default: () => ({ results: [], nextPage: '' }),
});
@@ -0,0 +1,12 @@
import { atomFamily } from 'recoil';
export type ChannelWritingActivityType = {
threadId: string;
userId: string;
name: string;
};
export const ChannelWritingActivityState = atomFamily<ChannelWritingActivityType[], string>({
key: 'ChannelWritingActivityState',
default: [],
});
@@ -0,0 +1,36 @@
import { atom, atomFamily, selectorFamily } from 'recoil';
import _ from 'lodash';
import { ChannelType } from 'app/features/channels/types/channel';
type ChannelsListContextType = { companyId: string; workspaceId: string };
export const ChannelsState = atom<ChannelType[]>({
key: 'ChannelsState',
default: [],
});
export const ChannelSelector = selectorFamily<ChannelType | undefined, string>({
key: 'ChannelSelector',
get:
channelId =>
({ get }) => {
const channels = get(ChannelsState);
return _.find(channels, { id: channelId });
},
});
export const MineChannelsState = atomFamily<ChannelType[], ChannelsListContextType>({
key: 'MineChannelsState',
default: [],
});
export const ReachableChannelsState = atomFamily<ChannelType[], ChannelsListContextType>({
key: 'ReachableChannelsState',
default: [],
});
export const DirectChannelsState = atomFamily<ChannelType[], string>({
key: 'DirectChannelsState',
default: [],
});
@@ -0,0 +1,61 @@
import { ChannelMemberType } from 'app/features/channel-members/types/channel-member-types';
import { UserType } from 'app/features/users/types/user';
import _ from 'lodash';
export type ChannelType = {
company_id?: string;
workspace_id?: string | null; //Null for direct messages
type?: string;
id?: string;
icon?: string;
name?: string;
description?: string;
channel_group?: string;
visibility?: 'public' | 'direct' | 'private';
is_default?: boolean;
members?: string[];
owner?: string;
members_count?: number;
guests_count?: number;
messages_count?: number;
archived?: false | true;
archivation_date?: number; //Timestamp
user_member?: ChannelMemberType;
connectors?: string[];
last_activity?: number;
last_message?: {
date: number;
sender: string;
title: string;
text: string;
};
stats?: {
members: number;
messages: number;
};
users?: UserType[];
is_readonly?: boolean;
};
export const createDirectChannelFromUsers = (companyId: string, users: UserType[]): ChannelType => {
users = _.uniqBy(users, 'id');
const id = users.map(u => u.id).join('_') + '_frontend';
return {
company_id: companyId,
workspace_id: 'direct',
visibility: 'direct',
id: id,
members: users.map(u => u.id).filter(a => a) as string[],
owner: users[0].id,
members_count: users.length,
guests_count: 0,
messages_count: 0,
archived: false,
user_member: {
user_id: users[0].id,
channel_id: id,
},
connectors: [],
users: users,
};
};
@@ -0,0 +1,15 @@
import { ChannelMemberType } from 'app/features/channel-members/types/channel-member-types';
import { ChannelType } from './channel';
export type ChannelsReachableGetResponse = { resources: ChannelType[] };
export type ChannelsReachableInviteUserResponse = { resource: ChannelMemberType };
export type ChannelsReachableInviteUserRequest = {
resource: {
user_id: string;
};
};
export type ChannelsReachableRemoveUserResponse = {
status: 'success' | 'error';
errors: string[]; //List of errors
};
@@ -0,0 +1,12 @@
// Channel visibility utils
export function isPublicChannel(visibility: string): boolean {
return visibility === 'public' ? true : false;
}
export function isPrivateChannel(visibility: string): boolean {
return visibility === 'private' ? true : false;
}
export function isDirectChannel(visibility: string): boolean {
return visibility === 'direct' ? true : false;
}