chore: merge from haodong code
This commit is contained in:
@@ -3,7 +3,7 @@ import { nanoid } from "@reduxjs/toolkit";
|
|||||||
import baseQuery from "./base.query";
|
import baseQuery from "./base.query";
|
||||||
import { setAuthData, updateToken, resetAuthData, updateInitialized } from "../slices/auth.data";
|
import { setAuthData, updateToken, resetAuthData, updateInitialized } from "../slices/auth.data";
|
||||||
import BASE_URL, { KEY_DEVICE_KEY } from "../config";
|
import BASE_URL, { KEY_DEVICE_KEY } from "../config";
|
||||||
import { AuthData } from '../../types/auth';
|
import { AuthData } from "../../types/auth";
|
||||||
|
|
||||||
const getDeviceId = () => {
|
const getDeviceId = () => {
|
||||||
let d = localStorage.getItem(KEY_DEVICE_KEY);
|
let d = localStorage.getItem(KEY_DEVICE_KEY);
|
||||||
@@ -18,7 +18,7 @@ export const authApi = createApi({
|
|||||||
reducerPath: "authApi",
|
reducerPath: "authApi",
|
||||||
baseQuery,
|
baseQuery,
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
login: builder.mutation({
|
login: builder.mutation<AuthData, string>({
|
||||||
query: (credentials) => ({
|
query: (credentials) => ({
|
||||||
url: "token/login",
|
url: "token/login",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -30,17 +30,21 @@ export const authApi = createApi({
|
|||||||
}),
|
}),
|
||||||
transformResponse: (data: AuthData) => {
|
transformResponse: (data: AuthData) => {
|
||||||
const { avatar_updated_at } = data.user;
|
const { avatar_updated_at } = data.user;
|
||||||
data.user.avatar =
|
return {
|
||||||
avatar_updated_at == 0
|
...data,
|
||||||
? ""
|
user: {
|
||||||
: `${BASE_URL}/resource/avatar?uid=${data.user.uid}&t=${avatar_updated_at}`;
|
...data.user,
|
||||||
return data;
|
avatar:
|
||||||
|
avatar_updated_at == 0
|
||||||
|
? ""
|
||||||
|
: `${BASE_URL}/resource/avatar?uid=${data.user.uid}&t=${avatar_updated_at}`
|
||||||
|
}
|
||||||
|
};
|
||||||
},
|
},
|
||||||
async onQueryStarted(params, { dispatch, queryFulfilled }) {
|
async onQueryStarted(params, { dispatch, queryFulfilled }) {
|
||||||
try {
|
try {
|
||||||
const { data } = await queryFulfilled;
|
const { data } = await queryFulfilled;
|
||||||
if (data) {
|
if (data) {
|
||||||
console.log("login resp", data);
|
|
||||||
dispatch(setAuthData(data));
|
dispatch(setAuthData(data));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -119,7 +123,7 @@ export const authApi = createApi({
|
|||||||
},
|
},
|
||||||
url: `user/send_login_magic_link?email=${encodeURIComponent(email)}`,
|
url: `user/send_login_magic_link?email=${encodeURIComponent(email)}`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
responseHandler: (response) => response.text()
|
responseHandler: (response: Response) => response.text()
|
||||||
// body: { email }
|
// body: { email }
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { KEY_EXPIRE, KEY_PWA_INSTALLED, KEY_REFRESH_TOKEN, KEY_TOKEN, KEY_UID } from "../config";
|
import { KEY_EXPIRE, KEY_PWA_INSTALLED, KEY_REFRESH_TOKEN, KEY_TOKEN, KEY_UID } from "../config";
|
||||||
import { AuthData, AuthToken } from "../../types/auth";
|
import { AuthData, AuthToken, User } from "../../types/auth";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
uid: string | null;
|
uid: string | null;
|
||||||
|
user: User | null;
|
||||||
token: string | null;
|
token: string | null;
|
||||||
expireTime: string | number; // todo
|
expireTime: number;
|
||||||
refreshToken: string | null;
|
refreshToken: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: State = {
|
const initialState: State = {
|
||||||
initialized: true,
|
initialized: true,
|
||||||
uid: null,
|
uid: null,
|
||||||
|
user: null,
|
||||||
token: localStorage.getItem(KEY_TOKEN),
|
token: localStorage.getItem(KEY_TOKEN),
|
||||||
expireTime: localStorage.getItem(KEY_EXPIRE) || +new Date(),
|
expireTime: Number(localStorage.getItem(KEY_EXPIRE) || +new Date()),
|
||||||
refreshToken: localStorage.getItem(KEY_REFRESH_TOKEN)
|
refreshToken: localStorage.getItem(KEY_REFRESH_TOKEN)
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptyState: State = {
|
const emptyState: State = {
|
||||||
initialized: true,
|
initialized: true,
|
||||||
uid: null,
|
uid: null,
|
||||||
|
user: null,
|
||||||
token: null,
|
token: null,
|
||||||
expireTime: +new Date(),
|
expireTime: +new Date(),
|
||||||
refreshToken: null
|
refreshToken: null
|
||||||
@@ -30,20 +33,20 @@ const authDataSlice = createSlice({
|
|||||||
name: "authData",
|
name: "authData",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setAuthData(state, action: PayloadAction<AuthData>) {
|
setAuthData(state, { payload }: PayloadAction<AuthData>) {
|
||||||
const {
|
const {
|
||||||
initialized = true,
|
initialized = true,
|
||||||
user: { uid },
|
user: { uid },
|
||||||
token,
|
token,
|
||||||
refresh_token,
|
refresh_token,
|
||||||
expired_in = 0
|
expired_in = 0
|
||||||
} = action.payload;
|
} = payload;
|
||||||
state.initialized = initialized;
|
state.initialized = initialized;
|
||||||
state.uid = `${uid}`;
|
state.uid = `${uid}`;
|
||||||
|
state.user = payload.user;
|
||||||
state.token = token;
|
state.token = token;
|
||||||
state.refreshToken = refresh_token;
|
state.refreshToken = refresh_token;
|
||||||
// 当前时间往后推expire时长
|
// 当前时间往后推expire时长
|
||||||
console.log("expire", expired_in);
|
|
||||||
const expireTime = +new Date() + Number(expired_in) * 1000;
|
const expireTime = +new Date() + Number(expired_in) * 1000;
|
||||||
state.expireTime = expireTime;
|
state.expireTime = expireTime;
|
||||||
// set local data
|
// set local data
|
||||||
@@ -53,7 +56,6 @@ const authDataSlice = createSlice({
|
|||||||
localStorage.setItem(KEY_UID, `${uid}`);
|
localStorage.setItem(KEY_UID, `${uid}`);
|
||||||
},
|
},
|
||||||
resetAuthData() {
|
resetAuthData() {
|
||||||
console.log("clear auth data");
|
|
||||||
// remove local data
|
// remove local data
|
||||||
localStorage.removeItem(KEY_EXPIRE);
|
localStorage.removeItem(KEY_EXPIRE);
|
||||||
localStorage.removeItem(KEY_TOKEN);
|
localStorage.removeItem(KEY_TOKEN);
|
||||||
|
|||||||
+23
-23
@@ -1,11 +1,15 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import BASE_URL from '../config';
|
import BASE_URL from "../config";
|
||||||
import { getNonNullValues } from '../../common/utils';
|
import { getNonNullValues } from "../../common/utils";
|
||||||
import { Channel, UpdateChannelDTO, UpdatePinnedMessageDTO } from '../../types/channel';
|
import { Channel, UpdateChannelDTO, UpdatePinnedMessageDTO } from "../../types/channel";
|
||||||
|
|
||||||
|
interface StoredChannel extends Channel {
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
ids: number[];
|
ids: number[];
|
||||||
byId: { [id: number]: Channel | undefined };
|
byId: { [id: number]: StoredChannel | undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: State = {
|
const initialState: State = {
|
||||||
@@ -23,16 +27,15 @@ const channelsSlice = createSlice({
|
|||||||
fullfillChannels(state, action: PayloadAction<Channel[]>) {
|
fullfillChannels(state, action: PayloadAction<Channel[]>) {
|
||||||
const channels = action.payload || [];
|
const channels = action.payload || [];
|
||||||
state.ids = channels.map(({ gid }) => gid);
|
state.ids = channels.map(({ gid }) => gid);
|
||||||
state.byId = Object.fromEntries(
|
channels.forEach((c) => {
|
||||||
channels.map((c) => {
|
state.byId[c.gid] = {
|
||||||
const { gid, avatar_updated_at } = c;
|
...c,
|
||||||
c.icon =
|
icon:
|
||||||
avatar_updated_at == 0
|
c.avatar_updated_at == 0
|
||||||
? ''
|
? ""
|
||||||
: `${BASE_URL}/resource/group_avatar?gid=${gid}&t=${avatar_updated_at}`;
|
: `${BASE_URL}/resource/group_avatar?gid=${c.gid}&t=${c.avatar_updated_at}`
|
||||||
return [gid, c];
|
};
|
||||||
})
|
});
|
||||||
);
|
|
||||||
},
|
},
|
||||||
addChannel(state, action: PayloadAction<Channel>) {
|
addChannel(state, action: PayloadAction<Channel>) {
|
||||||
const ch = action.payload;
|
const ch = action.payload;
|
||||||
@@ -44,26 +47,23 @@ const channelsSlice = createSlice({
|
|||||||
...ch,
|
...ch,
|
||||||
icon:
|
icon:
|
||||||
avatar_updated_at == 0
|
avatar_updated_at == 0
|
||||||
? ''
|
? ""
|
||||||
: `${BASE_URL}/resource/group_avatar?gid=${gid}&t=${avatar_updated_at}`
|
: `${BASE_URL}/resource/group_avatar?gid=${gid}&t=${avatar_updated_at}`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
updateChannel(state, action: PayloadAction<UpdateChannelDTO>) {
|
updateChannel(state, action: PayloadAction<UpdateChannelDTO>) {
|
||||||
const ignoreInPublic = ['add_member', 'remove_member'];
|
const ignoreInPublic = ["add_member", "remove_member"];
|
||||||
const { gid, operation, members = [], ...rest } = action.payload;
|
const { gid, operation, members = [], ...rest } = action.payload;
|
||||||
const currChannel = state.byId[gid];
|
const currChannel = state.byId[gid];
|
||||||
if (
|
if (!currChannel || (currChannel.is_public && ignoreInPublic.includes(operation))) return;
|
||||||
!currChannel ||
|
|
||||||
(currChannel.is_public && ignoreInPublic.includes(operation))
|
|
||||||
) return;
|
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case 'remove_member': {
|
case "remove_member": {
|
||||||
state.byId[gid]!.members = state.byId[gid]!.members.filter(
|
state.byId[gid]!.members = state.byId[gid]!.members.filter(
|
||||||
(id) => members.findIndex((uid) => uid == id) == -1
|
(id) => members.findIndex((uid) => uid == id) == -1
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'add_member': {
|
case "add_member": {
|
||||||
const currs = state.byId[gid]!.members;
|
const currs = state.byId[gid]!.members;
|
||||||
const _set = new Set([...currs, ...members]);
|
const _set = new Set([...currs, ...members]);
|
||||||
state.byId[gid]!.members = Array.from(_set);
|
state.byId[gid]!.members = Array.from(_set);
|
||||||
|
|||||||
+40
-36
@@ -1,12 +1,13 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import { MuteChannel, MuteUser } from "../../types/sse";
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
usersVersion: number;
|
usersVersion: number;
|
||||||
afterMid: number;
|
afterMid: number;
|
||||||
readUsers: {};
|
readUsers: { [uid: number]: number };
|
||||||
readChannels: {};
|
readChannels: { [gid: number]: number };
|
||||||
muteUsers: {};
|
muteUsers: { [uid: number]: { expired_at: number } | undefined };
|
||||||
muteChannels: {};
|
muteChannels: { [gid: number]: { expired_at: number } | undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: State = {
|
const initialState: State = {
|
||||||
@@ -18,6 +19,13 @@ const initialState: State = {
|
|||||||
muteChannels: {}
|
muteChannels: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface UpdateMutePayload {
|
||||||
|
remove_users: number[];
|
||||||
|
remove_groups: number[];
|
||||||
|
add_users: MuteUser[];
|
||||||
|
add_groups: MuteChannel[];
|
||||||
|
}
|
||||||
|
|
||||||
const footprintSlice = createSlice({
|
const footprintSlice = createSlice({
|
||||||
name: "footprint",
|
name: "footprint",
|
||||||
initialState,
|
initialState,
|
||||||
@@ -49,55 +57,51 @@ const footprintSlice = createSlice({
|
|||||||
updateAfterMid(state, action: PayloadAction<number>) {
|
updateAfterMid(state, action: PayloadAction<number>) {
|
||||||
state.afterMid = action.payload;
|
state.afterMid = action.payload;
|
||||||
},
|
},
|
||||||
updateMute(state, action) {
|
updateMute(state, action: PayloadAction<UpdateMutePayload>) {
|
||||||
const payload = action.payload || {};
|
const payload = action.payload || {};
|
||||||
Object.keys(payload).forEach((key) => {
|
Object.keys(payload).forEach((key) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "remove_users":
|
case "remove_users": {
|
||||||
{
|
const uids = payload.remove_users;
|
||||||
const uids = payload.remove_users;
|
uids.forEach((id) => {
|
||||||
uids.forEach((id) => {
|
delete state.muteUsers[id];
|
||||||
delete state.muteUsers[id];
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case "remove_groups":
|
}
|
||||||
{
|
case "remove_groups": {
|
||||||
const gids = payload.remove_groups;
|
const gids = payload.remove_groups;
|
||||||
gids.forEach((id) => {
|
gids.forEach((id) => {
|
||||||
delete state.muteChannels[id];
|
delete state.muteChannels[id];
|
||||||
});
|
});
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case "add_users":
|
}
|
||||||
{
|
case "add_users": {
|
||||||
const mutes = payload.add_users;
|
const mutes = payload.add_users;
|
||||||
mutes.forEach(({ uid, expired_in = null }) => {
|
mutes.forEach(({ uid, expired_at }) => {
|
||||||
state.muteUsers[uid] = { expired_in };
|
state.muteUsers[uid] = { expired_at };
|
||||||
});
|
});
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case "add_groups":
|
}
|
||||||
{
|
case "add_groups": {
|
||||||
const mutes = payload.add_groups;
|
const mutes = payload.add_groups;
|
||||||
mutes.forEach(({ gid, expired_in = null }) => {
|
mutes.forEach(({ gid, expired_at }) => {
|
||||||
state.muteChannels[gid] = { expired_in };
|
state.muteChannels[gid] = { expired_at };
|
||||||
});
|
});
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
updateReadUsers(state, action) {
|
updateReadUsers(state, action: PayloadAction<{ uid: number; mid: number }[]>) {
|
||||||
const reads = action.payload || [];
|
const reads = action.payload || [];
|
||||||
if (reads.length == 0) return;
|
if (reads.length == 0) return;
|
||||||
reads.forEach(({ uid, mid }) => {
|
reads.forEach(({ uid, mid }) => {
|
||||||
state.readUsers[uid] = mid;
|
state.readUsers[uid] = mid;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
updateReadChannels(state, action) {
|
updateReadChannels(state, action: PayloadAction<{ gid: number; mid: number }[]>) {
|
||||||
const reads = action.payload || [];
|
const reads = action.payload || [];
|
||||||
if (reads.length == 0) return;
|
if (reads.length == 0) return;
|
||||||
reads.forEach(({ gid, mid }) => {
|
reads.forEach(({ gid, mid }) => {
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
const initialState = {
|
|
||||||
|
interface State {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
logo: string;
|
||||||
|
inviteLink: {
|
||||||
|
link: string;
|
||||||
|
expire: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: State = {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
logo: "",
|
logo: "",
|
||||||
@@ -8,6 +19,7 @@ const initialState = {
|
|||||||
expire: 0
|
expire: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const serverSlice = createSlice({
|
const serverSlice = createSlice({
|
||||||
name: "server",
|
name: "server",
|
||||||
initialState,
|
initialState,
|
||||||
@@ -15,24 +27,28 @@ const serverSlice = createSlice({
|
|||||||
resetServer() {
|
resetServer() {
|
||||||
return initialState;
|
return initialState;
|
||||||
},
|
},
|
||||||
fullfillServer(state, action) {
|
fullfillServer(state, action: PayloadAction<State>) {
|
||||||
const {
|
const {
|
||||||
inviteLink = {
|
inviteLink = {
|
||||||
link: "",
|
link: "",
|
||||||
expire: 0
|
expire: 0
|
||||||
},
|
},
|
||||||
|
logo = "", // todo: check missed logo property
|
||||||
name = "",
|
name = "",
|
||||||
description = ""
|
description = ""
|
||||||
} = action.payload || {};
|
} = action.payload || {};
|
||||||
return { name, description, inviteLink };
|
return { name, logo, description, inviteLink };
|
||||||
},
|
},
|
||||||
updateInfo(state, action) {
|
updateInfo(state, action: PayloadAction<Partial<State>>) {
|
||||||
const values = action.payload || {};
|
const values = action.payload || {};
|
||||||
Object.keys(values).forEach((_key) => {
|
// todo: check and remove old logic
|
||||||
state[_key] = values[_key];
|
// Object.keys(values).forEach((_key) => {
|
||||||
});
|
// state[_key] = values[_key];
|
||||||
|
// });
|
||||||
|
state = { ...state, ...values };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { updateInfo, resetServer, fullfillServer } = serverSlice.actions;
|
export const { updateInfo, resetServer, fullfillServer } = serverSlice.actions;
|
||||||
export default serverSlice.reducer;
|
export default serverSlice.reducer;
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import { useState, useEffect, memo } from "react";
|
import { useState, useEffect, memo, SyntheticEvent, FC } from "react";
|
||||||
import { getInitials, getInitialsAvatar } from "../utils";
|
import { getInitials, getInitialsAvatar } from "../utils";
|
||||||
|
|
||||||
const Avatar = ({ url = "", name = "unkonw name", type = "user", ...rest }) => {
|
interface Props {
|
||||||
|
url?: string;
|
||||||
|
name?: string;
|
||||||
|
type?: "user" | "channel";
|
||||||
|
}
|
||||||
|
|
||||||
|
const Avatar: FC<Props> = ({ url = "", name = "unknown name", type = "user", ...rest }) => {
|
||||||
// console.log("avatar url", url);
|
// console.log("avatar url", url);
|
||||||
const [src, setSrc] = useState("");
|
const [src, setSrc] = useState("");
|
||||||
const handleError = (err) => {
|
|
||||||
|
const handleError = (err: SyntheticEvent<HTMLImageElement>) => {
|
||||||
console.log("load avatar error", err);
|
console.log("load avatar error", err);
|
||||||
const tmp = getInitialsAvatar({
|
const tmp = getInitialsAvatar({
|
||||||
initials: getInitials(name),
|
initials: getInitials(name),
|
||||||
@@ -13,6 +20,7 @@ const Avatar = ({ url = "", name = "unkonw name", type = "user", ...rest }) => {
|
|||||||
});
|
});
|
||||||
setSrc(tmp);
|
setSrc(tmp);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
const tmp = getInitialsAvatar({
|
const tmp = getInitialsAvatar({
|
||||||
@@ -26,6 +34,7 @@ const Avatar = ({ url = "", name = "unkonw name", type = "user", ...rest }) => {
|
|||||||
}
|
}
|
||||||
}, [url, name]);
|
}, [url, name]);
|
||||||
if (!src) return null;
|
if (!src) return null;
|
||||||
|
|
||||||
return <img src={src} onError={handleError} {...rest} />;
|
return <img src={src} onError={handleError} {...rest} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { ChangeEvent, FC, useState } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Avatar from "./Avatar";
|
import Avatar from "./Avatar";
|
||||||
import uploadIcon from "../../assets/icons/upload.image.svg?url";
|
import uploadIcon from "../../assets/icons/upload.image.svg?url";
|
||||||
@@ -61,21 +61,31 @@ const StyledWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function AvatarUploader({
|
interface Props {
|
||||||
|
url?: string;
|
||||||
|
name?: string;
|
||||||
|
type?: "user";
|
||||||
|
disabled?: boolean;
|
||||||
|
uploadImage: (file: File) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AvatarUploader: FC<Props> = ({
|
||||||
url = "",
|
url = "",
|
||||||
name = "",
|
name = "",
|
||||||
type = "user",
|
type = "user",
|
||||||
uploadImage,
|
uploadImage,
|
||||||
disabled = false
|
disabled = false
|
||||||
}) {
|
}) => {
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const handleUpload = async (evt) => {
|
const handleUpload = async (evt: ChangeEvent<HTMLInputElement>) => {
|
||||||
if (uploading) return;
|
if (uploading) return;
|
||||||
const [file] = evt.target.files;
|
if (!evt.target.files) return;
|
||||||
|
const [file] = Array.from(evt.target.files);
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
await uploadImage(file);
|
await uploadImage(file);
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<div className="avatar">
|
<div className="avatar">
|
||||||
@@ -87,6 +97,7 @@ export default function AvatarUploader({
|
|||||||
multiple={false}
|
multiple={false}
|
||||||
onChange={handleUpload}
|
onChange={handleUpload}
|
||||||
type="file"
|
type="file"
|
||||||
|
// todo: xss
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
name="avatar"
|
name="avatar"
|
||||||
id="avatar"
|
id="avatar"
|
||||||
@@ -97,4 +108,6 @@ export default function AvatarUploader({
|
|||||||
{!disabled && <img src={uploadIcon} alt="icon" className="icon" />}
|
{!disabled && <img src={uploadIcon} alt="icon" className="icon" />}
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default AvatarUploader;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { NavLink } from "react-router-dom";
|
import { NavLink } from "react-router-dom";
|
||||||
import ChannelModal from "./ChannelModal";
|
import ChannelModal from "./ChannelModal";
|
||||||
@@ -67,7 +67,11 @@ const Styled = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function BlankPlaceholder({ type = "chat" }) {
|
interface Props {
|
||||||
|
type?: "chat";
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlankPlaceholder: FC<Props> = ({ type = "chat" }) => {
|
||||||
const server = useAppSelector((store) => store.server);
|
const server = useAppSelector((store) => store.server);
|
||||||
const [inviteModalVisible, setInviteModalVisible] = useState(false);
|
const [inviteModalVisible, setInviteModalVisible] = useState(false);
|
||||||
const [createChannelVisible, setCreateChannelVisible] = useState(false);
|
const [createChannelVisible, setCreateChannelVisible] = useState(false);
|
||||||
@@ -83,7 +87,8 @@ export default function BlankPlaceholder({ type = "chat" }) {
|
|||||||
};
|
};
|
||||||
const chatTip =
|
const chatTip =
|
||||||
type == "chat" ? "Create a Channel to Start a Conversation" : "Send a Direct Message";
|
type == "chat" ? "Create a Channel to Start a Conversation" : "Send a Direct Message";
|
||||||
const chatHanlder = type == "chat" ? toggleChannelModalVisible : toggleContactListVisible;
|
const chatHandler = type == "chat" ? toggleChannelModalVisible : toggleContactListVisible;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Styled>
|
<Styled>
|
||||||
@@ -99,7 +104,7 @@ export default function BlankPlaceholder({ type = "chat" }) {
|
|||||||
<IconInvite className="icon" />
|
<IconInvite className="icon" />
|
||||||
<div className="txt">Invite your friends or teammates</div>
|
<div className="txt">Invite your friends or teammates</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="box" onClick={chatHanlder}>
|
<div className="box" onClick={chatHandler}>
|
||||||
<IconChat className="icon" />
|
<IconChat className="icon" />
|
||||||
<div className="txt">{chatTip}</div>
|
<div className="txt">{chatTip}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,7 +114,7 @@ export default function BlankPlaceholder({ type = "chat" }) {
|
|||||||
</a>
|
</a>
|
||||||
<NavLink to={"#"} className="box">
|
<NavLink to={"#"} className="box">
|
||||||
<IconAsk className="icon" />
|
<IconAsk className="icon" />
|
||||||
<div className="txt">Got quesions? Visit our help center </div>
|
<div className="txt">Got questions? Visit our help center </div>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
</Styled>
|
</Styled>
|
||||||
@@ -120,4 +125,6 @@ export default function BlankPlaceholder({ type = "chat" }) {
|
|||||||
{inviteModalVisible && <InviteModal closeModal={toggleInviteModalVisible} />}
|
{inviteModalVisible && <InviteModal closeModal={toggleInviteModalVisible} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default BlankPlaceholder;
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ import { useCreateChannelMutation } from "../../../app/services/channel";
|
|||||||
import { useAppSelector } from "../../../app/store";
|
import { useAppSelector } from "../../../app/store";
|
||||||
|
|
||||||
export default function ChannelModal({ personal = false, closeModal }) {
|
export default function ChannelModal({ personal = false, closeModal }) {
|
||||||
const { conactsData, loginUid } = useAppSelector((store) => {
|
const { contactsData, loginUid } = useAppSelector((store) => {
|
||||||
return { conactsData: store.contacts.byId, loginUid: store.authData.uid };
|
return { contactsData: store.contacts.byId, loginUid: store.authData.uid };
|
||||||
});
|
});
|
||||||
const navigateTo = useNavigate();
|
const navigateTo = useNavigate();
|
||||||
const [data, setData] = useState({
|
const [data, setData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
dsecription: "",
|
description: "",
|
||||||
members: [loginUid],
|
members: [loginUid],
|
||||||
is_public: !personal
|
is_public: !personal
|
||||||
});
|
});
|
||||||
@@ -71,14 +71,12 @@ export default function ChannelModal({ personal = false, closeModal }) {
|
|||||||
const { members } = data;
|
const { members } = data;
|
||||||
const { uid } = currentTarget.dataset;
|
const { uid } = currentTarget.dataset;
|
||||||
let tmp = members.includes(+uid) ? members.filter((m) => m != uid) : [...members, +uid];
|
let tmp = members.includes(+uid) ? members.filter((m) => m != uid) : [...members, +uid];
|
||||||
console.log(uid, currentTarget);
|
|
||||||
setData((prev) => {
|
setData((prev) => {
|
||||||
return { ...prev, members: tmp };
|
return { ...prev, members: tmp };
|
||||||
});
|
});
|
||||||
console.log({ data });
|
|
||||||
};
|
};
|
||||||
console.log("contacts", contacts);
|
|
||||||
const loginUser = conactsData[loginUid];
|
const loginUser = contactsData[loginUid];
|
||||||
if (!loginUser) return null;
|
if (!loginUser) return null;
|
||||||
const { name, members, is_public } = data;
|
const { name, members, is_public } = data;
|
||||||
return (
|
return (
|
||||||
@@ -98,7 +96,6 @@ export default function ChannelModal({ personal = false, closeModal }) {
|
|||||||
{contacts.map((u) => {
|
{contacts.map((u) => {
|
||||||
const { uid } = u;
|
const { uid } = u;
|
||||||
const checked = members.includes(uid);
|
const checked = members.includes(uid);
|
||||||
console.log({ checked });
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={uid}
|
key={uid}
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
|
import { FC, ReactElement } from "react";
|
||||||
import Tippy from "@tippyjs/react";
|
import Tippy from "@tippyjs/react";
|
||||||
import useContactOperation from "../../hook/useContactOperation";
|
import useContactOperation from "../../hook/useContactOperation";
|
||||||
import ContextMenu from "../ContextMenu";
|
import ContextMenu from "../ContextMenu";
|
||||||
|
|
||||||
export default function ContactContextMenu({ enable = false, uid, cid, visible, hide, children }) {
|
interface Props {
|
||||||
|
enable?: boolean;
|
||||||
|
uid: number;
|
||||||
|
cid: number;
|
||||||
|
visible: boolean;
|
||||||
|
hide: () => void;
|
||||||
|
children: ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContactContextMenu: FC<Props> = ({ enable = false, uid, cid, visible, hide, children }) => {
|
||||||
const {
|
const {
|
||||||
canCall,
|
canCall,
|
||||||
call,
|
call,
|
||||||
@@ -18,48 +28,48 @@ export default function ContactContextMenu({ enable = false, uid, cid, visible,
|
|||||||
cid
|
cid
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<Tippy
|
||||||
<Tippy
|
disabled={!enable}
|
||||||
disabled={!enable}
|
visible={visible}
|
||||||
visible={visible}
|
followCursor={"initial"}
|
||||||
followCursor={"initial"}
|
interactive
|
||||||
interactive
|
placement="right-start"
|
||||||
placement="right-start"
|
popperOptions={{ strategy: "fixed" }}
|
||||||
popperOptions={{ strategy: "fixed" }}
|
onClickOutside={hide}
|
||||||
onClickOutside={hide}
|
key={uid}
|
||||||
key={uid}
|
content={
|
||||||
content={
|
<ContextMenu
|
||||||
<ContextMenu
|
hideMenu={hide}
|
||||||
hideMenu={hide}
|
items={[
|
||||||
items={[
|
{
|
||||||
{
|
title: "Message",
|
||||||
title: "Message",
|
handler: startChat
|
||||||
handler: startChat
|
},
|
||||||
},
|
canCall && {
|
||||||
canCall && {
|
title: "Call",
|
||||||
title: "Call",
|
handler: call
|
||||||
handler: call
|
},
|
||||||
},
|
canCopyEmail && {
|
||||||
canCopyEmail && {
|
title: "Copy Email",
|
||||||
title: "Copy Email",
|
handler: copyEmail
|
||||||
handler: copyEmail
|
},
|
||||||
},
|
canRemoveFromChannel && {
|
||||||
canRemoveFromChannel && {
|
danger: true,
|
||||||
danger: true,
|
title: "Remove From Channel",
|
||||||
title: "Remove From Channel",
|
handler: removeFromChannel
|
||||||
handler: removeFromChannel
|
},
|
||||||
},
|
canRemove && {
|
||||||
canRemove && {
|
danger: true,
|
||||||
danger: true,
|
title: "Remove From Server",
|
||||||
title: "Remove From Server",
|
handler: removeUser
|
||||||
handler: removeUser
|
}
|
||||||
}
|
]}
|
||||||
]}
|
/>
|
||||||
/>
|
}
|
||||||
}
|
>
|
||||||
>
|
{children}
|
||||||
{children}
|
</Tippy>
|
||||||
</Tippy>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default ContactContextMenu;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { FC } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import Tippy from "@tippyjs/react";
|
import Tippy from "@tippyjs/react";
|
||||||
import IconOwner from "../../../assets/icons/owner.svg";
|
import IconOwner from "../../../assets/icons/owner.svg";
|
||||||
@@ -8,17 +9,29 @@ import StyledWrapper from "./styled";
|
|||||||
import useContextMenu from "../../hook/useContextMenu";
|
import useContextMenu from "../../hook/useContextMenu";
|
||||||
import { useAppSelector } from "../../../app/store";
|
import { useAppSelector } from "../../../app/store";
|
||||||
|
|
||||||
export default function Contact({
|
interface Props {
|
||||||
cid = null,
|
cid: number;
|
||||||
|
uid: number;
|
||||||
|
owner?: number;
|
||||||
|
dm?: boolean;
|
||||||
|
interactive?: boolean;
|
||||||
|
popover?: boolean;
|
||||||
|
compact?: boolean;
|
||||||
|
avatarSize?: number;
|
||||||
|
enableContextMenu?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Contact: FC<Props> = ({
|
||||||
|
cid,
|
||||||
|
uid,
|
||||||
owner = false,
|
owner = false,
|
||||||
dm = false,
|
dm = false,
|
||||||
interactive = true,
|
interactive = true,
|
||||||
uid = "",
|
|
||||||
popover = false,
|
popover = false,
|
||||||
compact = false,
|
compact = false,
|
||||||
avatarSize = 32,
|
avatarSize = 32,
|
||||||
enableContextMenu = false
|
enableContextMenu = false
|
||||||
}) {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { visible: contextMenuVisible, handleContextMenuEvent, hideContextMenu } = useContextMenu();
|
const { visible: contextMenuVisible, handleContextMenuEvent, hideContextMenu } = useContextMenu();
|
||||||
const curr = useAppSelector((store) => store.contacts.byId[uid]);
|
const curr = useAppSelector((store) => store.contacts.byId[uid]);
|
||||||
@@ -43,10 +56,10 @@ export default function Contact({
|
|||||||
content={<Profile uid={uid} type="card" cid={cid} />}
|
content={<Profile uid={uid} type="card" cid={cid} />}
|
||||||
>
|
>
|
||||||
<StyledWrapper
|
<StyledWrapper
|
||||||
onContextMenu={enableContextMenu ? handleContextMenuEvent : null}
|
|
||||||
size={avatarSize}
|
size={avatarSize}
|
||||||
onDoubleClick={dm ? handleDoubleClick : null}
|
|
||||||
className={`${interactive ? "interactive" : ""} ${compact ? "compact" : ""}`}
|
className={`${interactive ? "interactive" : ""} ${compact ? "compact" : ""}`}
|
||||||
|
onDoubleClick={dm ? handleDoubleClick : undefined}
|
||||||
|
onContextMenu={enableContextMenu ? handleContextMenuEvent : undefined}
|
||||||
>
|
>
|
||||||
<div className="avatar">
|
<div className="avatar">
|
||||||
<Avatar url={curr.avatar} name={curr.name} alt="avatar" />
|
<Avatar url={curr.avatar} name={curr.name} alt="avatar" />
|
||||||
@@ -58,4 +71,6 @@ export default function Contact({
|
|||||||
</Tippy>
|
</Tippy>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Contact;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FC, MouseEvent } from "react";
|
import { FC, MouseEvent } from "react";
|
||||||
import StyledMenu from "./styled/Menu";
|
import StyledMenu from "./styled/Menu";
|
||||||
|
|
||||||
interface Item {
|
export interface Item {
|
||||||
title: string;
|
title: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
handler: (e: MouseEvent) => void;
|
handler: (e: MouseEvent) => void;
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DeleteMessageConfirmModal: FC<Props> = ({ closeModal, mids = [] }) => {
|
const DeleteMessageConfirmModal: FC<Props> = ({ closeModal, mids = [] }) => {
|
||||||
// const dispatch = useDispatch();
|
const mid_arr: number[] = mids ? (Array.isArray(mids) ? mids : [mids]) : [];
|
||||||
const mid_arr = mids ? (Array.isArray(mids) ? mids : [mids]) : [];
|
|
||||||
const [ids] = useState(mid_arr);
|
const [ids] = useState(mid_arr);
|
||||||
const { deleteMessage, isDeleting } = useDeleteMessage();
|
const { deleteMessage, isDeleting } = useDeleteMessage();
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
@@ -40,7 +39,7 @@ const DeleteMessageConfirmModal: FC<Props> = ({ closeModal, mids = [] }) => {
|
|||||||
ids.length > 1 ? "these messages" : "this message"
|
ids.length > 1 ? "these messages" : "this message"
|
||||||
}?`}
|
}?`}
|
||||||
>
|
>
|
||||||
{ids.length == 1 && <PreviewMessage mid={ids[0]} />}
|
{ids.length === 1 && <PreviewMessage mid={ids[0]} />}
|
||||||
</StyledModal>
|
</StyledModal>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ const Styled = styled.div`
|
|||||||
|
|
||||||
export default function FAQ() {
|
export default function FAQ() {
|
||||||
const { data: serverVersion } = useGetServerVersionQuery();
|
const { data: serverVersion } = useGetServerVersionQuery();
|
||||||
console.log("build time", serverVersion);
|
|
||||||
return (
|
return (
|
||||||
<Styled>
|
<Styled>
|
||||||
<div className="item">Client Version: {process.env.VERSION}</div>
|
<div className="item">Client Version: {process.env.VERSION}</div>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// import React from 'react'
|
import { FC, ReactElement } from "react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import Styled from "./styled";
|
import Styled from "./styled";
|
||||||
import {
|
import {
|
||||||
VideoPreview,
|
VideoPreview,
|
||||||
@@ -13,13 +12,20 @@ import {
|
|||||||
} from "./preview";
|
} from "./preview";
|
||||||
import { getFileIcon, formatBytes } from "../../utils";
|
import { getFileIcon, formatBytes } from "../../utils";
|
||||||
import IconDownload from "../../../assets/icons/download.svg";
|
import IconDownload from "../../../assets/icons/download.svg";
|
||||||
|
import { useAppSelector } from "../../../app/store";
|
||||||
|
|
||||||
// todo
|
// todo: move to root file
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
const renderPreview = (data) => {
|
interface Data {
|
||||||
|
file_type: string;
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderPreview = (data: Data) => {
|
||||||
const { file_type, name, content } = data;
|
const { file_type, name, content } = data;
|
||||||
let preview = null;
|
let preview: null | ReactElement = null;
|
||||||
|
|
||||||
const checks = {
|
const checks = {
|
||||||
image: /^image/gi,
|
image: /^image/gi,
|
||||||
@@ -60,20 +66,31 @@ const renderPreview = (data) => {
|
|||||||
}
|
}
|
||||||
return preview;
|
return preview;
|
||||||
};
|
};
|
||||||
export default function FileBox({
|
|
||||||
preview = false,
|
interface Props {
|
||||||
|
preview?: boolean;
|
||||||
|
flex: boolean;
|
||||||
|
file_type: string;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
created_at: number;
|
||||||
|
from_uid: number;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileBox: FC<Props> = ({
|
||||||
|
preview,
|
||||||
flex,
|
flex,
|
||||||
file_type = "",
|
file_type,
|
||||||
name,
|
name,
|
||||||
size,
|
size,
|
||||||
created_at,
|
created_at,
|
||||||
from_uid,
|
from_uid,
|
||||||
content
|
content
|
||||||
}) {
|
}) => {
|
||||||
const fromUser = useSelector((store) => store.contacts.byId[from_uid]);
|
const fromUser = useAppSelector((store) => store.contacts.byId[from_uid]);
|
||||||
const icon = getFileIcon(file_type, name);
|
const icon = getFileIcon(file_type, name);
|
||||||
if (!content || !fromUser || !name) return null;
|
if (!content || !fromUser || !name) return null;
|
||||||
console.log("file content", content, name, flex);
|
|
||||||
const previewContent = renderPreview({ file_type, content, name });
|
const previewContent = renderPreview({ file_type, content, name });
|
||||||
const withPreview = preview && previewContent;
|
const withPreview = preview && previewContent;
|
||||||
return (
|
return (
|
||||||
@@ -101,4 +118,6 @@ export default function FileBox({
|
|||||||
{withPreview && <div className="preview">{previewContent}</div>}
|
{withPreview && <div className="preview">{previewContent}</div>}
|
||||||
</Styled>
|
</Styled>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default FileBox;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ReactEventHandler, useState } from "react";
|
import { FC, ReactEventHandler, useState } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
const Styled = styled.div`
|
const Styled = styled.div`
|
||||||
@@ -17,7 +17,11 @@ const Styled = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function Audio({ url = "" }) {
|
interface Props {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Audio: FC<Props> = ({ url }) => {
|
||||||
const [err, setErr] = useState(false);
|
const [err, setErr] = useState(false);
|
||||||
|
|
||||||
const handleError: ReactEventHandler<HTMLAudioElement> = (err) => {
|
const handleError: ReactEventHandler<HTMLAudioElement> = (err) => {
|
||||||
@@ -35,4 +39,6 @@ export default function Audio({ url = "" }) {
|
|||||||
)}
|
)}
|
||||||
</Styled>
|
</Styled>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Audio;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, FC } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
const Styled = styled.div`
|
const Styled = styled.div`
|
||||||
@@ -12,7 +12,11 @@ const Styled = styled.div`
|
|||||||
color: #eee;
|
color: #eee;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function Code({ url = "" }) {
|
interface Props {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Code: FC<Props> = ({ url }) => {
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getContent = async (url: string) => {
|
const getContent = async (url: string) => {
|
||||||
@@ -26,4 +30,6 @@ export default function Code({ url = "" }) {
|
|||||||
if (!content) return null;
|
if (!content) return null;
|
||||||
|
|
||||||
return <Styled>{content}</Styled>;
|
return <Styled>{content}</Styled>;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Code;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, FC } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
const Styled = styled.div`
|
const Styled = styled.div`
|
||||||
@@ -11,7 +11,11 @@ const Styled = styled.div`
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function Doc({ url = "" }) {
|
interface Props {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Doc: FC<Props> = ({ url }) => {
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getContent = async (url: string) => {
|
const getContent = async (url: string) => {
|
||||||
@@ -25,4 +29,6 @@ export default function Doc({ url = "" }) {
|
|||||||
if (!content) return null;
|
if (!content) return null;
|
||||||
|
|
||||||
return <Styled>{content}</Styled>;
|
return <Styled>{content}</Styled>;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Doc;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { FC } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
const Styled = styled.div`
|
const Styled = styled.div`
|
||||||
@@ -11,11 +11,18 @@ const Styled = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function Image({ url = "" }) {
|
interface Props {
|
||||||
|
url: string;
|
||||||
|
alt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Image: FC<Props> = ({ url, alt }) => {
|
||||||
if (!url) return null;
|
if (!url) return null;
|
||||||
return (
|
return (
|
||||||
<Styled>
|
<Styled>
|
||||||
<img src={url} />
|
<img src={url} alt={alt} />
|
||||||
</Styled>
|
</Styled>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Image;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
const Styled = styled.div`
|
const Styled = styled.div`
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@@ -10,14 +11,17 @@ const Styled = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function Pdf({ url = "" }) {
|
interface Props {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Pdf: FC<Props> = ({ url }) => {
|
||||||
// const [content, setContent] = useState("");
|
// const [content, setContent] = useState("");
|
||||||
// const [pageNumber, setPageNumber] = useState(1);
|
// const [pageNumber, setPageNumber] = useState(1);
|
||||||
// const [numPages, setNumPages] = useState(null);
|
// const [numPages, setNumPages] = useState(null);
|
||||||
// const onDocumentLoadSuccess = ({ numPages }) => {
|
// const onDocumentLoadSuccess = ({ numPages }) => {
|
||||||
// setNumPages(numPages);
|
// setNumPages(numPages);
|
||||||
// };
|
// };
|
||||||
if (!url) return null;
|
|
||||||
return (
|
return (
|
||||||
<Styled>
|
<Styled>
|
||||||
<embed src={url} type="application/pdf" />
|
<embed src={url} type="application/pdf" />
|
||||||
@@ -26,4 +30,6 @@ export default function Pdf({ url = "" }) {
|
|||||||
</Document> */}
|
</Document> */}
|
||||||
</Styled>
|
</Styled>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Pdf;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { FC } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
const Styled = styled.div`
|
const Styled = styled.div`
|
||||||
@@ -10,11 +10,16 @@ const Styled = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function Video({ url = "" }) {
|
interface Props {
|
||||||
if (!url) return null;
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Video: FC<Props> = ({ url }) => {
|
||||||
return (
|
return (
|
||||||
<Styled>
|
<Styled>
|
||||||
<video controls src={url} />
|
<video controls src={url} />
|
||||||
</Styled>
|
</Styled>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Video;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, FC } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { CircularProgressbar, buildStyles } from "react-circular-progressbar";
|
import { CircularProgressbar, buildStyles } from "react-circular-progressbar";
|
||||||
import "react-circular-progressbar/dist/styles.css";
|
import "react-circular-progressbar/dist/styles.css";
|
||||||
@@ -33,17 +33,25 @@ const Styled = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function ImageMessage({
|
interface Props {
|
||||||
|
uploading: boolean;
|
||||||
|
progress: number;
|
||||||
|
thumbnail: string;
|
||||||
|
download: string;
|
||||||
|
content: string;
|
||||||
|
properties: { width: number; height: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImageMessage: FC<Props> = ({
|
||||||
uploading,
|
uploading,
|
||||||
progress,
|
progress,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
download,
|
download,
|
||||||
content,
|
content,
|
||||||
properties = {}
|
properties
|
||||||
}) {
|
}) => {
|
||||||
const [url, setUrl] = useState(thumbnail);
|
const [url, setUrl] = useState(thumbnail);
|
||||||
const { width = 0, height = 0 } = getDefaultSize(properties);
|
const { width = 0, height = 0 } = getDefaultSize(properties);
|
||||||
console.log("image props", properties, width, height);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newUrl = thumbnail;
|
const newUrl = thumbnail;
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
@@ -64,10 +72,7 @@ export default function ImageMessage({
|
|||||||
<CircularProgressbar
|
<CircularProgressbar
|
||||||
value={progress}
|
value={progress}
|
||||||
strokeWidth={50}
|
strokeWidth={50}
|
||||||
styles={buildStyles({
|
styles={buildStyles({ strokeLinecap: "butt" })}
|
||||||
storke: "#000",
|
|
||||||
strokeLinecap: "butt"
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,4 +90,6 @@ export default function ImageMessage({
|
|||||||
/>
|
/>
|
||||||
</Styled>
|
</Styled>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default ImageMessage;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, FC } from "react";
|
||||||
import IconGithub from "../../assets/icons/github.svg";
|
import IconGithub from "../../assets/icons/github.svg";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { KEY_LOCAL_MAGIC_TOKEN } from "../../app/config";
|
import { KEY_LOCAL_MAGIC_TOKEN } from "../../app/config";
|
||||||
@@ -17,6 +17,7 @@ const StyledSocialButton = styled(Button)`
|
|||||||
color: #344054;
|
color: #344054;
|
||||||
border: 1px solid #d0d5dd;
|
border: 1px solid #d0d5dd;
|
||||||
background: none !important;
|
background: none !important;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
@@ -28,7 +29,7 @@ type Props = {
|
|||||||
type?: "login" | "register";
|
type?: "login" | "register";
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function GithubLoginButton({ type = "login", client_id }: Props) {
|
const GithubLoginButton: FC<Props> = ({ type = "login", client_id }) => {
|
||||||
//拿本地存的magic token
|
//拿本地存的magic token
|
||||||
const magic_token = localStorage.getItem(KEY_LOCAL_MAGIC_TOKEN);
|
const magic_token = localStorage.getItem(KEY_LOCAL_MAGIC_TOKEN);
|
||||||
const [login, { isLoading, isSuccess, error }] = useLoginMutation();
|
const [login, { isLoading, isSuccess, error }] = useLoginMutation();
|
||||||
@@ -66,7 +67,7 @@ export default function GithubLoginButton({ type = "login", client_id }: Props)
|
|||||||
}
|
}
|
||||||
}, [error]);
|
}, [error]);
|
||||||
const handleGithubLogin = () => {
|
const handleGithubLogin = () => {
|
||||||
location.href = `http://github.com/login/oauth/authorize?client_id=${client_id}`;
|
location.href = `https://github.com/login/oauth/authorize?client_id=${client_id}`;
|
||||||
// console.log("github login");
|
// console.log("github login");
|
||||||
};
|
};
|
||||||
// console.log("google login ", loaded);
|
// console.log("google login ", loaded);
|
||||||
@@ -76,4 +77,5 @@ export default function GithubLoginButton({ type = "login", client_id }: Props)
|
|||||||
{` ${type === "login" ? "Sign in" : "Sign up"} with Github`}
|
{` ${type === "login" ? "Sign in" : "Sign up"} with Github`}
|
||||||
</StyledSocialButton>
|
</StyledSocialButton>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
export default GithubLoginButton;
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { useRef, useState, useEffect } from "react";
|
import { useRef, useState, useEffect, FC } from "react";
|
||||||
import styled, { keyframes } from "styled-components";
|
import styled, { keyframes } from "styled-components";
|
||||||
import { useOutsideClick, useKey } from "rooks";
|
import { useOutsideClick, useKey } from "rooks";
|
||||||
import { Ring } from "@uiball/loaders";
|
import { Ring } from "@uiball/loaders";
|
||||||
import Modal from "./Modal";
|
import Modal from "./Modal";
|
||||||
|
|
||||||
const AniFadeIn = keyframes`
|
const AniFadeIn = keyframes`
|
||||||
from{
|
from {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
to{
|
to {
|
||||||
background: rgba(1, 1, 1, 0.9);
|
background: rgba(1, 1, 1, 0.9);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledWrapper = styled.div`
|
const StyledWrapper = styled.div`
|
||||||
@@ -23,6 +23,7 @@ const StyledWrapper = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
@@ -30,14 +31,17 @@ const StyledWrapper = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
> .loading {
|
> .loading {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 40%;
|
top: 40%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
> .image {
|
> .image {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 70vw;
|
max-width: 70vw;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
@@ -46,13 +50,16 @@ const StyledWrapper = styled.div`
|
|||||||
object-fit: contain; */
|
object-fit: contain; */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.loading .image img {
|
&.loading .image img {
|
||||||
filter: blur(2px);
|
filter: blur(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.origin {
|
.origin {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -61,11 +68,26 @@ const StyledWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function ImagePreviewModal({ download = true, data, closeModal }) {
|
export interface PreviewImageData {
|
||||||
|
originUrl: string;
|
||||||
|
thumbnail: string;
|
||||||
|
downloadLink: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
download?: boolean;
|
||||||
|
data?: PreviewImageData;
|
||||||
|
closeModal: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImagePreviewModal: FC<Props> = ({ download = true, data, closeModal }) => {
|
||||||
const [url, setUrl] = useState(data?.thumbnail);
|
const [url, setUrl] = useState(data?.thumbnail);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const wrapperRef = useRef();
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
useOutsideClick(wrapperRef, closeModal);
|
useOutsideClick(wrapperRef, closeModal);
|
||||||
|
|
||||||
useKey(
|
useKey(
|
||||||
"Escape",
|
"Escape",
|
||||||
() => {
|
() => {
|
||||||
@@ -74,6 +96,7 @@ export default function ImagePreviewModal({ download = true, data, closeModal })
|
|||||||
},
|
},
|
||||||
{ eventTypes: ["keyup"] }
|
{ eventTypes: ["keyup"] }
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
const { originUrl } = data;
|
const { originUrl } = data;
|
||||||
@@ -126,4 +149,6 @@ export default function ImagePreviewModal({ download = true, data, closeModal })
|
|||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default ImagePreviewModal;
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
// import { useState, useEffect } from "react";
|
import { FC } from "react";
|
||||||
import styled, { keyframes } from "styled-components";
|
import styled, { keyframes } from "styled-components";
|
||||||
import { Ring } from "@uiball/loaders";
|
import { Ring } from "@uiball/loaders";
|
||||||
import Button from "./styled/Button";
|
import Button from "./styled/Button";
|
||||||
import useLogout from "../hook/useLogout";
|
import useLogout from "../hook/useLogout";
|
||||||
|
|
||||||
const DelayVisible = keyframes`
|
const DelayVisible = keyframes`
|
||||||
from{
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
to{
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledWrapper = styled.div`
|
const StyledWrapper = styled.div`
|
||||||
@@ -21,17 +21,15 @@ const StyledWrapper = styled.div`
|
|||||||
gap: 15px;
|
gap: 15px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
&.fullscreen {
|
&.fullscreen {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
.loading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.reload {
|
.reload {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
||||||
&.visible {
|
&.visible {
|
||||||
animation: ${DelayVisible} 1s forwards;
|
animation: ${DelayVisible} 1s forwards;
|
||||||
animation-delay: 30s;
|
animation-delay: 30s;
|
||||||
@@ -39,20 +37,26 @@ const StyledWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function Loading({ reload = false, fullscreen = false }) {
|
interface Props {
|
||||||
|
reload?: boolean;
|
||||||
|
fullscreen?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Loading: FC<Props> = ({ reload = false, fullscreen = false }) => {
|
||||||
const { clearLocalData } = useLogout();
|
const { clearLocalData } = useLogout();
|
||||||
const handleReload = () => {
|
const handleReload = () => {
|
||||||
clearLocalData();
|
clearLocalData();
|
||||||
// todo: only firefox has argument
|
location.reload();
|
||||||
location.reload(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className={fullscreen ? "fullscreen" : ""}>
|
<StyledWrapper className={fullscreen ? "fullscreen" : ""}>
|
||||||
<Ring className="loading" size={40} lineWeight={5} speed={2} color="black" />
|
<Ring size={40} lineWeight={5} speed={2} color="black" />
|
||||||
<Button className={`reload danger ${reload ? "visible" : ""}`} onClick={handleReload}>
|
<Button className={`reload danger ${reload ? "visible" : ""}`} onClick={handleReload}>
|
||||||
Reload
|
Reload
|
||||||
</Button>
|
</Button>
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Loading;
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { FC, ReactNode, useEffect, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
export default function Modal({ id = "root-modal", mask = true, children }) {
|
interface Props {
|
||||||
const [wrapper, setWrapper] = useState(null);
|
id?: string;
|
||||||
// let eleRef = useRef(null);
|
mask?: boolean;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: check memory leak
|
||||||
|
const Modal: FC<Props> = ({ id = "root-modal", mask = true, children }) => {
|
||||||
|
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const modalRoot = document.getElementById(id);
|
const modalRoot = document.getElementById(id);
|
||||||
|
if (!modalRoot) return;
|
||||||
if (mask) {
|
if (mask) {
|
||||||
modalRoot.classList.add("mask");
|
modalRoot.classList.add("mask");
|
||||||
}
|
}
|
||||||
@@ -17,7 +25,9 @@ export default function Modal({ id = "root-modal", mask = true, children }) {
|
|||||||
modalRoot.removeChild(wrapper);
|
modalRoot.removeChild(wrapper);
|
||||||
};
|
};
|
||||||
}, [id, mask]);
|
}, [id, mask]);
|
||||||
|
|
||||||
if (!wrapper) return null;
|
if (!wrapper) return null;
|
||||||
console.log("create portal");
|
|
||||||
return createPortal(children, wrapper);
|
return createPortal(children, wrapper);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { FC, ReactElement } from "react";
|
||||||
import EmojiThumbUp from "../../assets/icons/emoji.thumb.up.svg";
|
import EmojiThumbUp from "../../assets/icons/emoji.thumb.up.svg";
|
||||||
import EmojiThumbDown from "../../assets/icons/emoji.thumb.down.svg";
|
import EmojiThumbDown from "../../assets/icons/emoji.thumb.down.svg";
|
||||||
import EmojiSmile from "../../assets/icons/emoji.smile.svg";
|
import EmojiSmile from "../../assets/icons/emoji.smile.svg";
|
||||||
@@ -7,7 +8,18 @@ import EmojiHeart from "../../assets/icons/emoji.heart.svg";
|
|||||||
import EmojiRocket from "../../assets/icons/emoji.rocket.svg";
|
import EmojiRocket from "../../assets/icons/emoji.rocket.svg";
|
||||||
import EmojiLook from "../../assets/icons/emoji.look.svg";
|
import EmojiLook from "../../assets/icons/emoji.look.svg";
|
||||||
|
|
||||||
const Emojis = {
|
interface Emojis {
|
||||||
|
"👍": ReactElement;
|
||||||
|
"👎": ReactElement;
|
||||||
|
"😄": ReactElement;
|
||||||
|
"👀": ReactElement;
|
||||||
|
"🚀": ReactElement;
|
||||||
|
"❤️": ReactElement;
|
||||||
|
"🙁": ReactElement;
|
||||||
|
"🎉": ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emojis: Emojis = {
|
||||||
"👍": <EmojiThumbUp className="emoji" />,
|
"👍": <EmojiThumbUp className="emoji" />,
|
||||||
"👎": <EmojiThumbDown className="emoji" />,
|
"👎": <EmojiThumbDown className="emoji" />,
|
||||||
"😄": <EmojiSmile className="emoji" />,
|
"😄": <EmojiSmile className="emoji" />,
|
||||||
@@ -18,8 +30,13 @@ const Emojis = {
|
|||||||
"🎉": <EmojiCelebrate className="emoji" />
|
"🎉": <EmojiCelebrate className="emoji" />
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ReactionItem({ native = "" }) {
|
interface Props {
|
||||||
if (!native || !Emojis[native]) return null;
|
native?: keyof Emojis;
|
||||||
|
|
||||||
return Emojis[native];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ReactionItem: FC<Props> = ({ native }) => {
|
||||||
|
if (!native) return null;
|
||||||
|
return emojis[native] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReactionItem;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const StyledWrapper = styled.div`
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
/* gap: 20px; */
|
/* gap: 20px; */
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
box-shadow: 0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06);
|
box-shadow: 0 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06);
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
.txt {
|
.txt {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@@ -37,7 +37,7 @@ const StyledWrapper = styled.div`
|
|||||||
background: #1fe1f9;
|
background: #1fe1f9;
|
||||||
border: 1px solid #1fe1f9;
|
border: 1px solid #1fe1f9;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
&.reset {
|
&.reset {
|
||||||
background: none;
|
background: none;
|
||||||
|
|||||||
@@ -34,11 +34,10 @@ const StyledWrapper = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export default function Search() {
|
export default function Search() {
|
||||||
console.log("searching");
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<div className="search">
|
<div className="search">
|
||||||
<img src={searchIcon} />
|
<img src={searchIcon} alt="search icon" />
|
||||||
<input placeholder="Search..." className="input" />
|
<input placeholder="Search..." className="input" />
|
||||||
</div>
|
</div>
|
||||||
<Tooltip tip="More" placement="bottom">
|
<Tooltip tip="More" placement="bottom">
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default function Server() {
|
|||||||
<NavLink to={`/setting?f=${pathname}`}>
|
<NavLink to={`/setting?f=${pathname}`}>
|
||||||
<div className="server">
|
<div className="server">
|
||||||
<div className="logo">
|
<div className="logo">
|
||||||
<img src={logo} />
|
<img alt="logo" src={logo} />
|
||||||
</div>
|
</div>
|
||||||
<div className="info">
|
<div className="info">
|
||||||
<h3 className="name" title={description}>
|
<h3 className="name" title={description}>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { FC, ReactElement } from "react";
|
||||||
import styled, { createGlobalStyle } from "styled-components";
|
import styled, { createGlobalStyle } from "styled-components";
|
||||||
import Tippy from "@tippyjs/react";
|
import Tippy from "@tippyjs/react";
|
||||||
import { roundArrow } from "tippy.js";
|
import { Placement, roundArrow } from "tippy.js";
|
||||||
import "tippy.js/dist/svg-arrow.css";
|
import "tippy.js/dist/svg-arrow.css";
|
||||||
import { updateUserGuide } from "../../../app/slices/ui";
|
import { updateUserGuide } from "../../../app/slices/ui";
|
||||||
import steps from "./steps";
|
import steps from "./steps";
|
||||||
import { useAppDispatch, useAppSelector } from "../../../app/store";
|
import { useAppDispatch } from "../../../app/store";
|
||||||
|
|
||||||
const Styled = styled.div`
|
const Styled = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -57,15 +58,21 @@ const OverrideTippyArrowColor = createGlobalStyle`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function UserGuide({ step = 1, placement = "right-start", delay = null, children }) {
|
interface Props {
|
||||||
|
step?: number;
|
||||||
|
placement: Placement;
|
||||||
|
delay?: number | [number | null, number | null];
|
||||||
|
children: ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserGuide: FC<Props> = ({ step = 1, placement = "right-start", delay, children }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { visible, step: reduxStep } = useAppSelector((store) => store.ui.userGuide);
|
|
||||||
const currStep = steps[step - 1];
|
const currStep = steps[step - 1];
|
||||||
const isLastStep = steps.length == step;
|
const isLastStep = steps.length == step;
|
||||||
// if (!visible) return children;
|
// if (!visible) return children;
|
||||||
if (!currStep) return null;
|
if (!currStep) return null;
|
||||||
const { title, description } = currStep;
|
const { title, description } = currStep;
|
||||||
const defaultDuration = [300, 250];
|
const defaultDuration: [number, number] = [300, 250];
|
||||||
const handleSkip = () => {
|
const handleSkip = () => {
|
||||||
// to do
|
// to do
|
||||||
dispatch(updateUserGuide({ visible: false }));
|
dispatch(updateUserGuide({ visible: false }));
|
||||||
@@ -104,4 +111,6 @@ export default function UserGuide({ step = 1, placement = "right-start", delay =
|
|||||||
</Tippy>
|
</Tippy>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default UserGuide;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FC, ReactElement } from "react";
|
import { FC, ReactNode } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
const Styled = styled.div`
|
const Styled = styled.div`
|
||||||
@@ -43,18 +43,12 @@ const Styled = styled.div`
|
|||||||
interface Props {
|
interface Props {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
buttons: ReactElement[] | ReactElement;
|
buttons?: ReactNode;
|
||||||
children?: ReactElement;
|
children?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledModal: FC<Props> = ({
|
const StyledModal: FC<Props> = ({ title = "", description = "", buttons, children, ...props }) => {
|
||||||
title = "",
|
|
||||||
description = "",
|
|
||||||
buttons = null,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<Styled {...props}>
|
<Styled {...props}>
|
||||||
{title && <h3 className="title">{title}</h3>}
|
{title && <h3 className="title">{title}</h3>}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useRef, useEffect } from "react";
|
import { useRef, useEffect } from "react";
|
||||||
// import { useDebounce } from "rooks";
|
// import { useDebounce } from "rooks";
|
||||||
|
|
||||||
function useChatScroll(deps = []) {
|
function useChatScroll<T extends HTMLElement>() {
|
||||||
// todo: ref should be initialized to null
|
const ref = useRef<T>(null);
|
||||||
const ref = useRef();
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// console.log("chat scroll", ref);
|
// console.log("chat scroll", ref);
|
||||||
// if (ref.current) {
|
// if (ref.current) {
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import { useState } from "react";
|
import { useState, MouseEvent, ReactElement } from "react";
|
||||||
import { hideAll } from "tippy.js";
|
import { hideAll } from "tippy.js";
|
||||||
import Tippy from "@tippyjs/react";
|
import Tippy from "@tippyjs/react";
|
||||||
import Menu from "../component/ContextMenu";
|
import Menu, { Item } from "../component/ContextMenu";
|
||||||
|
|
||||||
|
interface ContextMenuProps {
|
||||||
|
key: string | number;
|
||||||
|
children: ReactElement;
|
||||||
|
items: Item[];
|
||||||
|
}
|
||||||
|
|
||||||
export default function useContextMenu(placement = "right-start") {
|
export default function useContextMenu(placement = "right-start") {
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
// for tippy.js
|
// for tippy.js
|
||||||
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
||||||
const handleContextMenuEvent = (evt) => {
|
const handleContextMenuEvent = (evt: MouseEvent) => {
|
||||||
// console.log("context menu event", evt, evt.currentTarget);
|
|
||||||
hideAll();
|
hideAll();
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const { currentTarget, clientX, clientY } = evt;
|
const { currentTarget, clientX, clientY } = evt;
|
||||||
@@ -22,14 +27,14 @@ export default function useContextMenu(placement = "right-start") {
|
|||||||
y = top - clientY;
|
y = top - clientY;
|
||||||
}
|
}
|
||||||
setOffset({ x, y });
|
setOffset({ x, y });
|
||||||
|
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
console.log("offset", x, y);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const hideContextMenu = () => {
|
const hideContextMenu = () => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
};
|
};
|
||||||
const ContextMenu = ({ key, items, children }) => {
|
|
||||||
|
const ContextMenu = ({ key, items, children }: ContextMenuProps) => {
|
||||||
return (
|
return (
|
||||||
<Tippy
|
<Tippy
|
||||||
visible={visible}
|
visible={visible}
|
||||||
@@ -45,6 +50,7 @@ export default function useContextMenu(placement = "right-start") {
|
|||||||
</Tippy>
|
</Tippy>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
offset,
|
offset,
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
// import second from 'first'
|
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
import { removeMessage } from "../../app/slices/message";
|
import { removeMessage } from "../../app/slices/message";
|
||||||
import { removeChannelMsg } from "../../app/slices/message.channel";
|
import { removeChannelMsg } from "../../app/slices/message.channel";
|
||||||
import { removeUserMsg } from "../../app/slices/message.user";
|
import { removeUserMsg } from "../../app/slices/message.user";
|
||||||
export default function useRemoveLocalMessage({ context = "user", id = 0 }) {
|
import { useAppDispatch } from "../../app/store";
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
// todo: check usage
|
||||||
|
interface Props {
|
||||||
|
context: "user" | "channel";
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useRemoveLocalMessage({ context = "user", id = 0 }: Props) {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const removeContextMessage = context == "channel" ? removeChannelMsg : removeUserMsg;
|
const removeContextMessage = context == "channel" ? removeChannelMsg : removeUserMsg;
|
||||||
const removeLocalMessage = (mid) => {
|
|
||||||
|
return (mid: number) => {
|
||||||
dispatch(removeContextMessage({ id, mid }));
|
dispatch(removeContextMessage({ id, mid }));
|
||||||
dispatch(removeMessage(mid));
|
dispatch(removeMessage(mid));
|
||||||
};
|
};
|
||||||
return removeLocalMessage;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,34 @@
|
|||||||
// import second from 'first'
|
import toast from "react-hot-toast";
|
||||||
import { removeReplyingMessage, addReplyingMessage } from "../../app/slices/message";
|
import { removeReplyingMessage, addReplyingMessage } from "../../app/slices/message";
|
||||||
import { useSendChannelMsgMutation } from "../../app/services/channel";
|
import { useSendChannelMsgMutation } from "../../app/services/channel";
|
||||||
import { useSendMsgMutation } from "../../app/services/contact";
|
import { useSendMsgMutation } from "../../app/services/contact";
|
||||||
import { useReplyMessageMutation } from "../../app/services/message";
|
import { useReplyMessageMutation } from "../../app/services/message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useAppDispatch, useAppSelector } from "../../app/store";
|
||||||
import toast from "react-hot-toast";
|
|
||||||
export default function useSendMessage(props) {
|
interface Props {
|
||||||
|
context: "user" | "channel";
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SendMessagesDTO {
|
||||||
|
type: "text";
|
||||||
|
content: string;
|
||||||
|
users: number[];
|
||||||
|
channels: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SendMessageDTO {
|
||||||
|
type: "text";
|
||||||
|
content: string;
|
||||||
|
properties: object;
|
||||||
|
reply_mid?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useSendMessage(props?: Props) {
|
||||||
const { context = "user", from = null, to = null } = props || {};
|
const { context = "user", from = null, to = null } = props || {};
|
||||||
const dispatch = useDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const stageFiles = useSelector((store) => store.ui.uploadFiles[`${context}_${to}`] || []);
|
const stageFiles = useAppSelector((store) => store.ui.uploadFiles[`${context}_${to}`] || []);
|
||||||
const [replyMessage, { isError: replyErr, isLoading: replying, isSuccess: replySuccess }] =
|
const [replyMessage, { isError: replyErr, isLoading: replying, isSuccess: replySuccess }] =
|
||||||
useReplyMessageMutation();
|
useReplyMessageMutation();
|
||||||
const [
|
const [
|
||||||
@@ -18,7 +38,12 @@ export default function useSendMessage(props) {
|
|||||||
const [sendUserMsg, { isLoading: userSending, isSuccess: userSuccess, isError: userError }] =
|
const [sendUserMsg, { isLoading: userSending, isSuccess: userSuccess, isError: userError }] =
|
||||||
useSendMsgMutation();
|
useSendMsgMutation();
|
||||||
const sendFn = context == "user" ? sendUserMsg : sendChannelMsg;
|
const sendFn = context == "user" ? sendUserMsg : sendChannelMsg;
|
||||||
const sendMessages = async ({ type = "text", content, users = [], channels = [] }) => {
|
const sendMessages = async ({
|
||||||
|
type = "text",
|
||||||
|
content,
|
||||||
|
users = [],
|
||||||
|
channels = []
|
||||||
|
}: SendMessagesDTO) => {
|
||||||
if (users.length) {
|
if (users.length) {
|
||||||
for await (const uid of users) {
|
for await (const uid of users) {
|
||||||
await sendUserMsg({
|
await sendUserMsg({
|
||||||
@@ -42,9 +67,9 @@ export default function useSendMessage(props) {
|
|||||||
type = "text",
|
type = "text",
|
||||||
content,
|
content,
|
||||||
properties = {},
|
properties = {},
|
||||||
reply_mid = null,
|
reply_mid,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}: SendMessageDTO) => {
|
||||||
if (reply_mid) {
|
if (reply_mid) {
|
||||||
removeReplying();
|
removeReplying();
|
||||||
await replyMessage({
|
await replyMessage({
|
||||||
@@ -66,7 +91,7 @@ export default function useSendMessage(props) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const setReplying = (mid) => {
|
const setReplying = (mid: number) => {
|
||||||
if (stageFiles.length !== 0) {
|
if (stageFiles.length !== 0) {
|
||||||
toast.error("Only text is supported when replying a message");
|
toast.error("Only text is supported when replying a message");
|
||||||
return;
|
return;
|
||||||
|
|||||||
Vendored
+73
-1
@@ -1 +1,73 @@
|
|||||||
/// <reference types="react-scripts" />
|
/// <reference types="node" />
|
||||||
|
/// <reference types="react" />
|
||||||
|
/// <reference types="react-dom" />
|
||||||
|
|
||||||
|
declare namespace NodeJS {
|
||||||
|
interface ProcessEnv {
|
||||||
|
readonly NODE_ENV: "development" | "production" | "test";
|
||||||
|
readonly PUBLIC_URL: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.avif" {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.bmp" {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.gif" {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.jpg" {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.jpeg" {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.png" {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.webp" {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix type error: TS2307: Cannot find module '...svg?url' or its corresponding type declarations.
|
||||||
|
declare module "*.svg?url" {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.svg" {
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement> & { title?: string }>;
|
||||||
|
export default ReactComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.module.css" {
|
||||||
|
const classes: { readonly [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.module.scss" {
|
||||||
|
const classes: { readonly [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.module.sass" {
|
||||||
|
const classes: { readonly [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import Overview from "./Overview";
|
import Overview from "./Overview";
|
||||||
import ManageMembers from "../../common/component/ManageMembers";
|
import ManageMembers from "../../common/component/ManageMembers";
|
||||||
const useNavs = (channelId) => {
|
|
||||||
const navs = [
|
const useNavs = (channelId: number) => {
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
title: "General",
|
title: "General",
|
||||||
items: [
|
items: [
|
||||||
@@ -36,7 +37,6 @@ const useNavs = (channelId) => {
|
|||||||
// ],
|
// ],
|
||||||
// },
|
// },
|
||||||
];
|
];
|
||||||
return navs;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useNavs;
|
export default useNavs;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export interface AuthToken {
|
export interface AuthToken {
|
||||||
// common
|
// common
|
||||||
server_id: string;
|
server_id: string;
|
||||||
@@ -7,11 +6,9 @@ export interface AuthToken {
|
|||||||
expired_in: number;
|
expired_in: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: check gender values
|
|
||||||
export type Gender = 0 | 1;
|
export type Gender = 0 | 1;
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
// avatar: string; // todo: check transform data in redux slice
|
|
||||||
uid: number;
|
uid: number;
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
+4
-14
@@ -1,18 +1,8 @@
|
|||||||
|
export interface ChannelMember {}
|
||||||
|
|
||||||
export interface ChannelMember {
|
export interface Message {}
|
||||||
|
|
||||||
}
|
export type ContentType = "text/plain" | "text/markdown" | "rustchat/file" | "rustchat/archive";
|
||||||
|
|
||||||
// todo: check message fields
|
|
||||||
export interface ChannelMessage {
|
|
||||||
mid: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ContentType =
|
|
||||||
'text/plain' |
|
|
||||||
'text/markdown' |
|
|
||||||
'rustchat/file' |
|
|
||||||
'rustchat/archive';
|
|
||||||
|
|
||||||
export interface PinnedMessage {
|
export interface PinnedMessage {
|
||||||
mid: number;
|
mid: number;
|
||||||
@@ -38,7 +28,7 @@ export interface Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateChannelDTO {
|
export interface UpdateChannelDTO {
|
||||||
operation: 'add_member' | 'remove_member';
|
operation: "add_member" | "remove_member";
|
||||||
members?: number[];
|
members?: number[];
|
||||||
|
|
||||||
// type = 'group_changed'
|
// type = 'group_changed'
|
||||||
|
|||||||
Vendored
+4
-4
@@ -1,10 +1,10 @@
|
|||||||
import { PrecacheEntry } from "workbox-precaching/src/_types";
|
|
||||||
import localforage from "localforage";
|
|
||||||
|
|
||||||
export declare global {
|
export declare global {
|
||||||
|
import { PrecacheEntry } from "workbox-precaching/src/_types";
|
||||||
|
import localforage from "localforage";
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
__WB_MANIFEST: Array<PrecacheEntry | string>;
|
__WB_MANIFEST: Array<PrecacheEntry | string>;
|
||||||
skipWaiting: () => void;
|
skipWaiting: () => void;
|
||||||
CACHE: { [key: string]: typeof localforage };
|
CACHE: { [key: string]: typeof localforage | undefined };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+103
-63
@@ -1,24 +1,24 @@
|
|||||||
import { User } from './auth';
|
import { User } from "./auth";
|
||||||
import { Channel, ContentType } from './channel';
|
import { Channel, ContentType } from "./channel";
|
||||||
|
|
||||||
export interface ReadyEvent {
|
export interface ReadyEvent {
|
||||||
type: 'ready';
|
type: "ready";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UsersSnapshotEvent {
|
export interface UsersSnapshotEvent {
|
||||||
type: 'users_snapshot';
|
type: "users_snapshot";
|
||||||
users: User[];
|
users: User[];
|
||||||
version: number;
|
version: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: check if create_by field exists
|
// todo: check if create_by field exists
|
||||||
export interface UserLog extends Omit<User, 'create_by'> {
|
export interface UserLog extends Omit<User, "create_by"> {
|
||||||
log_id: number;
|
log_id: number;
|
||||||
action: 'create' | 'update' | 'delete';
|
action: "create" | "update" | "delete";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UsersLogEvent {
|
export interface UsersLogEvent {
|
||||||
type: 'users_log';
|
type: "users_log";
|
||||||
logs: UserLog[];
|
logs: UserLog[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,87 +28,126 @@ export interface UserState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UsersStateEvent {
|
export interface UsersStateEvent {
|
||||||
type: 'users_state';
|
type: "users_state";
|
||||||
users: UserState[];
|
users: UserState[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UsersStateChangedEvent extends UserState{
|
interface UsersStateChangedEvent extends UserState {
|
||||||
type: 'users_state_changed';
|
type: "users_state_changed";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserSettingsEvent {
|
export interface MuteUser {
|
||||||
type: 'user_settings';
|
uid: number;
|
||||||
mute_users?: { uid: number; expired_at: number; }[];
|
expired_at: number;
|
||||||
mute_groups?: { gid: number; expired_at: number; }[];
|
|
||||||
read_index_users?: { uid: number; mid: number; }[];
|
|
||||||
read_index_groups?: { gid: number; mid: number; }[];
|
|
||||||
burn_after_reading_users?: { uid: number; expires_in: number; }[];
|
|
||||||
burn_after_reading_groups?: { gid: number; expires_in: number; }[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserSettingsChangedEvent {
|
export interface MuteChannel {
|
||||||
type: 'user_settings_changed';
|
gid: number;
|
||||||
|
expired_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserSettingsEvent {
|
||||||
|
type: "user_settings";
|
||||||
|
mute_users?: MuteUser[];
|
||||||
|
mute_groups?: MuteChannel[];
|
||||||
|
read_index_users?: { uid: number; mid: number }[];
|
||||||
|
read_index_groups?: { gid: number; mid: number }[];
|
||||||
|
burn_after_reading_users?: { uid: number; expires_in: number }[];
|
||||||
|
burn_after_reading_groups?: { gid: number; expires_in: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserSettingsChangedEvent {
|
||||||
|
type: "user_settings_changed";
|
||||||
from_device?: string;
|
from_device?: string;
|
||||||
add_mute_users?: { uid: number; expired_at: number; }[];
|
add_mute_users?: MuteUser[];
|
||||||
remove_mute_users?: number[];
|
remove_mute_users?: number[];
|
||||||
add_mute_groups?: { gid: number; expired_at: number; }[];
|
add_mute_groups?: MuteChannel[];
|
||||||
remove_mute_groups?: number[];
|
remove_mute_groups?: number[];
|
||||||
read_index_users?: { uid: number; mid: number; }[];
|
read_index_users?: { uid: number; mid: number }[];
|
||||||
read_index_groups?: { gid: number; mid: number; }[];
|
read_index_groups?: { gid: number; mid: number }[];
|
||||||
burn_after_reading_users?: { uid: number; expires_in: number; }[];
|
burn_after_reading_users?: { uid: number; expires_in: number }[];
|
||||||
burn_after_reading_groups?: { gid: number; expires_in: number; }[];
|
burn_after_reading_groups?: { gid: number; expires_in: number }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RelatedGroupsEvent {
|
export interface RelatedGroupsEvent {
|
||||||
type: 'related_groups';
|
type: "related_groups";
|
||||||
groups: Channel[];
|
groups: Channel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatEvent {
|
export interface NormalMessage {
|
||||||
type: 'chat';
|
type: "normal";
|
||||||
|
properties: {};
|
||||||
|
content_type: ContentType;
|
||||||
|
content: string;
|
||||||
|
expires_in: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditReactionDetail {
|
||||||
|
properties: {};
|
||||||
|
content_type: ContentType;
|
||||||
|
content: string;
|
||||||
|
type: "edit";
|
||||||
|
}
|
||||||
|
export interface LikeReactionDetail {
|
||||||
|
action: string;
|
||||||
|
type: "like";
|
||||||
|
}
|
||||||
|
export interface DeleteReactionDetail {
|
||||||
|
type: "delete";
|
||||||
|
}
|
||||||
|
export interface ReactionMessage {
|
||||||
|
type: "reaction";
|
||||||
|
mid: number; // original message id
|
||||||
|
detail: EditReactionDetail | LikeReactionDetail | DeleteReactionDetail;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReplyMessage {
|
||||||
|
type: "reply";
|
||||||
|
mid: number; // original message id
|
||||||
|
properties: {};
|
||||||
|
content_type: ContentType;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatEvent {
|
||||||
|
type: "chat";
|
||||||
mid: number;
|
mid: number;
|
||||||
from_uid: number;
|
from_uid: number;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
target: { uid: number };
|
target: { uid: number };
|
||||||
detail: {
|
detail: NormalMessage | ReactionMessage | ReplyMessage;
|
||||||
properties: {};
|
|
||||||
content: string;
|
|
||||||
content_type: ContentType;
|
|
||||||
expires_in: number;
|
|
||||||
type: 'normal';
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KickEvent {
|
interface KickEvent {
|
||||||
type: 'kick';
|
type: "kick";
|
||||||
reason: string;
|
reason: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserJoinedGroupEvent {
|
interface UserJoinedGroupEvent {
|
||||||
type: 'user_joined_group';
|
type: "user_joined_group";
|
||||||
gid: number;
|
gid: number;
|
||||||
uid: number[];
|
uid: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserLeavedGroupEvent {
|
interface UserLeavedGroupEvent {
|
||||||
type: 'user_leaved_group';
|
type: "user_leaved_group";
|
||||||
gid: number;
|
gid: number;
|
||||||
uid: number[];
|
uid: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JoinedGroupEvent {
|
interface JoinedGroupEvent {
|
||||||
type: 'joined_group';
|
type: "joined_group";
|
||||||
group: Channel;
|
group: Channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KickFromGroupEvent {
|
interface KickFromGroupEvent {
|
||||||
type: 'kick_from_group';
|
type: "kick_from_group";
|
||||||
gid: number;
|
gid: number;
|
||||||
reason: string;
|
reason: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GroupChangedEvent {
|
interface GroupChangedEvent {
|
||||||
type: 'group_changed';
|
type: "group_changed";
|
||||||
gid: number;
|
gid: number;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -117,7 +156,7 @@ interface GroupChangedEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface PinnedMessageUpdatedEvent {
|
interface PinnedMessageUpdatedEvent {
|
||||||
type: 'pinned_message_updated';
|
type: "pinned_message_updated";
|
||||||
gid: number;
|
gid: number;
|
||||||
mid: number;
|
mid: number;
|
||||||
msg: {
|
msg: {
|
||||||
@@ -127,28 +166,29 @@ interface PinnedMessageUpdatedEvent {
|
|||||||
properties: {};
|
properties: {};
|
||||||
content: string;
|
content: string;
|
||||||
content_type: ContentType;
|
content_type: ContentType;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HeartbeatEvent {
|
interface HeartbeatEvent {
|
||||||
type: 'heartbeat';
|
type: "heartbeat";
|
||||||
time: number;
|
time: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServerEvent = ReadyEvent |
|
export type ServerEvent =
|
||||||
UsersSnapshotEvent |
|
| ReadyEvent
|
||||||
UsersLogEvent |
|
| UsersSnapshotEvent
|
||||||
UsersStateEvent |
|
| UsersLogEvent
|
||||||
UsersStateChangedEvent |
|
| UsersStateEvent
|
||||||
UserSettingsEvent |
|
| UsersStateChangedEvent
|
||||||
UserSettingsChangedEvent |
|
| UserSettingsEvent
|
||||||
RelatedGroupsEvent |
|
| UserSettingsChangedEvent
|
||||||
ChatEvent |
|
| RelatedGroupsEvent
|
||||||
KickEvent |
|
| ChatEvent
|
||||||
UserJoinedGroupEvent |
|
| KickEvent
|
||||||
UserLeavedGroupEvent |
|
| UserJoinedGroupEvent
|
||||||
JoinedGroupEvent |
|
| UserLeavedGroupEvent
|
||||||
KickFromGroupEvent |
|
| JoinedGroupEvent
|
||||||
GroupChangedEvent |
|
| KickFromGroupEvent
|
||||||
PinnedMessageUpdatedEvent |
|
| GroupChangedEvent
|
||||||
HeartbeatEvent;
|
| PinnedMessageUpdatedEvent
|
||||||
|
| HeartbeatEvent;
|
||||||
|
|||||||
Reference in New Issue
Block a user