mirror of
https://github.com/revoltchat/revite.git
synced 2024-11-25 08:30:58 -05:00
Port friends menu over.
This commit is contained in:
parent
0ff78787a8
commit
0a0c00fe58
18 changed files with 452 additions and 30 deletions
|
@ -5,7 +5,8 @@
|
||||||
"build": "rimraf build && tsc && vite build",
|
"build": "rimraf build && tsc && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
|
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
|
||||||
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'"
|
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
|
@ -54,6 +55,7 @@
|
||||||
"markdown-it-emoji": "^2.0.0",
|
"markdown-it-emoji": "^2.0.0",
|
||||||
"markdown-it-sub": "^1.0.0",
|
"markdown-it-sub": "^1.0.0",
|
||||||
"markdown-it-sup": "^1.0.0",
|
"markdown-it-sup": "^1.0.0",
|
||||||
|
"preact-context-menu": "^0.1.5",
|
||||||
"preact-i18n": "^2.4.0-preactx",
|
"preact-i18n": "^2.4.0-preactx",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"prismjs": "^1.23.0",
|
"prismjs": "^1.23.0",
|
||||||
|
|
|
@ -17,7 +17,7 @@ export default styled.div<Props>`
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
background-color: var(--primary-background);
|
background-color: var(--primary-header);
|
||||||
background-size: cover !important;
|
background-size: cover !important;
|
||||||
background-position: center !important;
|
background-position: center !important;
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ export default styled.div<Props>`
|
||||||
` }
|
` }
|
||||||
|
|
||||||
${ props => props.placement === 'secondary' && css`
|
${ props => props.placement === 'secondary' && css`
|
||||||
|
background-color: var(--secondary-header);
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
` }
|
` }
|
||||||
`;
|
`;
|
||||||
|
|
43
src/components/ui/IconButton.tsx
Normal file
43
src/components/ui/IconButton.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import styled, { css } from "styled-components";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
type?: 'default' | 'circle'
|
||||||
|
}
|
||||||
|
|
||||||
|
const normal = `var(--secondary-foreground)`;
|
||||||
|
const hover = `var(--foreground)`;
|
||||||
|
|
||||||
|
export default styled.div<Props>`
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
cursor: pointer;
|
||||||
|
place-items: center;
|
||||||
|
|
||||||
|
fill: ${normal};
|
||||||
|
color: ${normal};
|
||||||
|
stroke: ${normal};
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: ${normal};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
fill: ${hover};
|
||||||
|
color: ${hover};
|
||||||
|
stroke: ${hover};
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: ${hover};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${ props => props.type === 'circle' && css`
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--secondary-header);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--primary-header);
|
||||||
|
}
|
||||||
|
` }
|
||||||
|
`;
|
|
@ -2,7 +2,7 @@ import Button from "./Button";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Children } from "../../types/Preact";
|
import { Children } from "../../types/Preact";
|
||||||
import { createPortal, useEffect } from "preact/compat";
|
import { createPortal, useEffect } from "preact/compat";
|
||||||
import styled, { keyframes } from "styled-components";
|
import styled, { css, keyframes } from "styled-components";
|
||||||
|
|
||||||
const open = keyframes`
|
const open = keyframes`
|
||||||
0% {opacity: 0;}
|
0% {opacity: 0;}
|
||||||
|
@ -48,6 +48,26 @@ const ModalContainer = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ModalContent = styled.div<{ [key in 'attachment' | 'noBackground' | 'border']?: boolean }>`
|
const ModalContent = styled.div<{ [key in 'attachment' | 'noBackground' | 'border']?: boolean }>`
|
||||||
|
border-radius: 8px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
${ props => !props.noBackground && css`
|
||||||
|
padding: 1.5em;
|
||||||
|
background: var(--secondary-header);
|
||||||
|
` }
|
||||||
|
|
||||||
|
${ props => props.attachment && css`
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
` }
|
||||||
|
|
||||||
|
${ props => props.border && css`
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid var(--secondary-background);
|
||||||
|
` }
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ModalActions = styled.div`
|
const ModalActions = styled.div`
|
||||||
|
@ -64,7 +84,8 @@ export interface Action {
|
||||||
text: Children;
|
text: Children;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
confirmation?: boolean;
|
confirmation?: boolean;
|
||||||
style?: 'default' | 'contrast' | 'error' | 'contrast-error';
|
contrast?: boolean;
|
||||||
|
error?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -123,7 +144,9 @@ export default function Modal(props: Props) {
|
||||||
{props.actions && (
|
{props.actions && (
|
||||||
<ModalActions>
|
<ModalActions>
|
||||||
{props.actions.map(x => (
|
{props.actions.map(x => (
|
||||||
<Button style={x.style ?? "contrast"}
|
<Button
|
||||||
|
contrast={x.contrast ?? true}
|
||||||
|
error={x.error ?? false}
|
||||||
onClick={x.onClick}
|
onClick={x.onClick}
|
||||||
disabled={props.disabled}>
|
disabled={props.disabled}>
|
||||||
{x.text}
|
{x.text}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import styled, { css } from "styled-components";
|
import styled, { css } from "styled-components";
|
||||||
import { Children } from "../../types/Preact";
|
import { Children } from "../../types/Preact";
|
||||||
|
import { Text } from 'preact-i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
error?: string;
|
||||||
block?: boolean;
|
block?: boolean;
|
||||||
error?: Children;
|
|
||||||
children?: Children;
|
children?: Children;
|
||||||
type?: "default" | "subtle" | "error";
|
type?: "default" | "subtle" | "error";
|
||||||
}
|
}
|
||||||
|
@ -45,7 +46,9 @@ export default function Overline(props: Props) {
|
||||||
<OverlineBase {...props}>
|
<OverlineBase {...props}>
|
||||||
{props.children}
|
{props.children}
|
||||||
{props.children && props.error && <> · </>}
|
{props.children && props.error && <> · </>}
|
||||||
{props.error && <Overline type="error">{props.error}</Overline>}
|
{props.error && <Overline type="error">
|
||||||
|
<Text id={`error.${props.error}`}>{props.error}</Text>
|
||||||
|
</Overline>}
|
||||||
</OverlineBase>
|
</OverlineBase>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ export type Screen =
|
||||||
{ type: "ban_member", target: Servers.Server, user: string }
|
{ type: "ban_member", target: Servers.Server, user: string }
|
||||||
)) |
|
)) |
|
||||||
({ id: "special_input" } & (
|
({ id: "special_input" } & (
|
||||||
{ type: "create_group" | "create_server" | "set_custom_status" } |
|
{ type: "create_group" | "create_server" | "set_custom_status" | "add_friend" } |
|
||||||
{ type: "create_channel", server: string }
|
{ type: "create_channel", server: string }
|
||||||
))
|
))
|
||||||
| {
|
| {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { Screen } from "./Intermediate";
|
import { Screen } from "./Intermediate";
|
||||||
|
|
||||||
import { ErrorModal } from "./modals/Error";
|
import { ErrorModal } from "./modals/Error";
|
||||||
|
import { InputModal } from "./modals/Input";
|
||||||
|
import { PromptModal } from "./modals/Prompt";
|
||||||
import { SignedOutModal } from "./modals/SignedOut";
|
import { SignedOutModal } from "./modals/SignedOut";
|
||||||
import { ClipboardModal } from "./modals/Clipboard";
|
import { ClipboardModal } from "./modals/Clipboard";
|
||||||
import { OnboardingModal } from "./modals/Onboarding";
|
import { OnboardingModal } from "./modals/Onboarding";
|
||||||
import { ModifyAccountModal } from "./modals/ModifyAccount";
|
import { ModifyAccountModal } from "./modals/ModifyAccount";
|
||||||
import { InputModal, SpecialInputModal } from "./modals/Input";
|
|
||||||
import { PromptModal, SpecialPromptModal } from "./modals/Prompt";
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
screen: Screen;
|
screen: Screen;
|
||||||
|
@ -19,12 +19,8 @@ export default function Modals({ screen, openScreen }: Props) {
|
||||||
switch (screen.id) {
|
switch (screen.id) {
|
||||||
case "_prompt":
|
case "_prompt":
|
||||||
return <PromptModal onClose={onClose} {...screen} />;
|
return <PromptModal onClose={onClose} {...screen} />;
|
||||||
case "special_prompt":
|
|
||||||
return <SpecialPromptModal onClose={onClose} {...screen} />;
|
|
||||||
case "_input":
|
case "_input":
|
||||||
return <InputModal onClose={onClose} {...screen} />;
|
return <InputModal onClose={onClose} {...screen} />;
|
||||||
case "special_input":
|
|
||||||
return <SpecialInputModal onClose={onClose} {...screen} />;
|
|
||||||
case "error":
|
case "error":
|
||||||
return <ErrorModal onClose={onClose} {...screen} />;
|
return <ErrorModal onClose={onClose} {...screen} />;
|
||||||
case "signed_out":
|
case "signed_out":
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { IntermediateContext, useIntermediate } from "./Intermediate";
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
|
|
||||||
import { UserPicker } from "./popovers/UserPicker";
|
import { UserPicker } from "./popovers/UserPicker";
|
||||||
|
import { SpecialInputModal } from "./modals/Input";
|
||||||
|
import { SpecialPromptModal } from "./modals/Prompt";
|
||||||
import { UserProfile } from "./popovers/UserProfile";
|
import { UserProfile } from "./popovers/UserProfile";
|
||||||
import { ImageViewer } from "./popovers/ImageViewer";
|
import { ImageViewer } from "./popovers/ImageViewer";
|
||||||
import { ChannelInfo } from "./popovers/ChannelInfo";
|
import { ChannelInfo } from "./popovers/ChannelInfo";
|
||||||
|
@ -21,6 +23,10 @@ export default function Popovers() {
|
||||||
return <ImageViewer {...screen} onClose={onClose} />;
|
return <ImageViewer {...screen} onClose={onClose} />;
|
||||||
case "channel_info":
|
case "channel_info":
|
||||||
return <ChannelInfo {...screen} onClose={onClose} />;
|
return <ChannelInfo {...screen} onClose={onClose} />;
|
||||||
|
case "special_prompt":
|
||||||
|
return <SpecialPromptModal onClose={onClose} {...screen} />;
|
||||||
|
case "special_input":
|
||||||
|
return <SpecialInputModal onClose={onClose} {...screen} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { AppContext } from "../../revoltjs/RevoltClient";
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
question: Children;
|
question: Children;
|
||||||
field: Children;
|
field?: Children;
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
callback: (value: string) => Promise<void>;
|
callback: (value: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -53,9 +53,9 @@ export function InputModal({
|
||||||
]}
|
]}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<Overline error={error} block>
|
{ field ? <Overline error={error} block>
|
||||||
{field}
|
{field}
|
||||||
</Overline>
|
</Overline> : (error && <Overline error={error} type="error" block />) }
|
||||||
<InputBox
|
<InputBox
|
||||||
value={value}
|
value={value}
|
||||||
onChange={e => setValue(e.currentTarget.value)}
|
onChange={e => setValue(e.currentTarget.value)}
|
||||||
|
@ -65,7 +65,7 @@ export function InputModal({
|
||||||
}
|
}
|
||||||
|
|
||||||
type SpecialProps = { onClose: () => void } & (
|
type SpecialProps = { onClose: () => void } & (
|
||||||
{ type: "create_group" | "create_server" | "set_custom_status" } |
|
{ type: "create_group" | "create_server" | "set_custom_status" | "add_friend" } |
|
||||||
{ type: "create_channel", server: string }
|
{ type: "create_channel", server: string }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -144,6 +144,15 @@ export function SpecialInputModal(props: SpecialProps) {
|
||||||
}
|
}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
case "add_friend": {
|
||||||
|
return <InputModal
|
||||||
|
onClose={onClose}
|
||||||
|
question={"Add Friend"}
|
||||||
|
callback={username =>
|
||||||
|
client.users.addFriend(username)
|
||||||
|
}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import styles from './Prompt.module.scss';
|
import styles from './Prompt.module.scss';
|
||||||
import { Children } from "../../../types/Preact";
|
import { Children } from "../../../types/Preact";
|
||||||
import { IntermediateContext, useIntermediate } from "../Intermediate";
|
import { useIntermediate } from "../Intermediate";
|
||||||
import InputBox from "../../../components/ui/InputBox";
|
import InputBox from "../../../components/ui/InputBox";
|
||||||
import Overline from "../../../components/ui/Overline";
|
import Overline from "../../../components/ui/Overline";
|
||||||
import UserIcon from "../../../components/common/UserIcon";
|
import UserIcon from "../../../components/common/UserIcon";
|
||||||
|
@ -82,7 +82,8 @@ export function SpecialPromptModal(props: SpecialProps) {
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
confirmation: true,
|
confirmation: true,
|
||||||
style: 'contrast-error',
|
contrast: true,
|
||||||
|
error: true,
|
||||||
text: <Text id="app.special.modals.actions.delete" />,
|
text: <Text id="app.special.modals.actions.delete" />,
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
|
@ -162,7 +163,8 @@ export function SpecialPromptModal(props: SpecialProps) {
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
text: <Text id="app.special.modals.actions.kick" />,
|
text: <Text id="app.special.modals.actions.kick" />,
|
||||||
style: 'contrast-error',
|
contrast: true,
|
||||||
|
error: true,
|
||||||
confirmation: true,
|
confirmation: true,
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
|
@ -200,7 +202,8 @@ export function SpecialPromptModal(props: SpecialProps) {
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
text: <Text id="app.special.modals.actions.ban" />,
|
text: <Text id="app.special.modals.actions.ban" />,
|
||||||
style: 'contrast-error',
|
contrast: true,
|
||||||
|
error: true,
|
||||||
confirmation: true,
|
confirmation: true,
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
|
|
4
src/lib/stopPropagation.ts
Normal file
4
src/lib/stopPropagation.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export const stopPropagation = (ev: JSX.TargetedMouseEvent<HTMLDivElement>, _consume?: any) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
};
|
|
@ -1,12 +1,22 @@
|
||||||
import { Docked, OverlappingPanels } from "react-overlapping-panels";
|
import { Docked, OverlappingPanels } from "react-overlapping-panels";
|
||||||
import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
|
import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
|
||||||
|
import Popovers from "../context/intermediate/Popovers";
|
||||||
import { Switch, Route } from "react-router-dom";
|
import { Switch, Route } from "react-router-dom";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
import LeftSidebar from "../components/navigation/LeftSidebar";
|
import LeftSidebar from "../components/navigation/LeftSidebar";
|
||||||
import RightSidebar from "../components/navigation/RightSidebar";
|
import RightSidebar from "../components/navigation/RightSidebar";
|
||||||
|
|
||||||
import Home from './home/Home';
|
import Home from './home/Home';
|
||||||
import Popovers from "../context/intermediate/Popovers";
|
import Friends from "./friends/Friends";
|
||||||
|
|
||||||
|
const Routes = styled.div`
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--primary-background);
|
||||||
|
`;
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
@ -16,12 +26,76 @@ export default function App() {
|
||||||
leftPanel={{ width: 292, component: <LeftSidebar /> }}
|
leftPanel={{ width: 292, component: <LeftSidebar /> }}
|
||||||
rightPanel={{ width: 240, component: <RightSidebar /> }}
|
rightPanel={{ width: 240, component: <RightSidebar /> }}
|
||||||
docked={isTouchscreenDevice ? Docked.None : Docked.Left}>
|
docked={isTouchscreenDevice ? Docked.None : Docked.Left}>
|
||||||
<Switch>
|
<Routes>
|
||||||
<Route path="/">
|
<Switch>
|
||||||
<Home />
|
<Route path="/friends">
|
||||||
</Route>
|
<Friends />
|
||||||
</Switch>
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/">
|
||||||
|
<Home />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</Routes>
|
||||||
<Popovers />
|
<Popovers />
|
||||||
</OverlappingPanels>
|
</OverlappingPanels>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* <Route path="/channel/:channel/message/:message">
|
||||||
|
<ChannelWrapper />
|
||||||
|
</Route>
|
||||||
|
<Route path="/server/:server/channel/:channel/settings/:page">
|
||||||
|
<ChannelSettings key="channel_settings" />
|
||||||
|
</Route>
|
||||||
|
<Route path="/server/:server/channel/:channel/settings">
|
||||||
|
<ChannelSettings key="channel_settings" />
|
||||||
|
</Route>
|
||||||
|
<Route path="/server/:server/settings/:page">
|
||||||
|
<ServerSettings key="channel_settings" />
|
||||||
|
</Route>
|
||||||
|
<Route path="/server/:server/settings">
|
||||||
|
<ServerSettings key="channel_settings" />
|
||||||
|
</Route>
|
||||||
|
<Route path="/channel/:channel/settings/:page">
|
||||||
|
<ChannelSettings key="channel_settings" />
|
||||||
|
</Route>
|
||||||
|
<Route path="/channel/:channel/settings">
|
||||||
|
<ChannelSettings key="channel_settings" />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/settings/:page">
|
||||||
|
<Settings key="settings" />
|
||||||
|
</Route>
|
||||||
|
<Route path="/settings">
|
||||||
|
<Settings key="settings" />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/server/:server/channel/:channel">
|
||||||
|
<ChannelWrapper />
|
||||||
|
</Route>
|
||||||
|
<Route path="/server/:server" />
|
||||||
|
<Route path="/channel/:channel">
|
||||||
|
<ChannelWrapper />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/friends">
|
||||||
|
<Friends />
|
||||||
|
</Route>
|
||||||
|
<Route path="/dev">
|
||||||
|
<Developer />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/open/:id">
|
||||||
|
<Open />
|
||||||
|
</Route>
|
||||||
|
{/*<Route path="/invite/:code">
|
||||||
|
<OpenInvite />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/">
|
||||||
|
<Home />
|
||||||
|
</Route>
|
||||||
|
*/
|
||||||
|
|
71
src/pages/friends/Friend.module.scss
Normal file
71
src/pages/friends/Friend.module.scss
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
.list {
|
||||||
|
padding: 16px;
|
||||||
|
user-select: none;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
&[data-empty="true"] {
|
||||||
|
img {
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
gap: 16px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend {
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
border-radius: 5px;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--secondary-background);
|
||||||
|
|
||||||
|
:global(.button) {
|
||||||
|
background-color: var(--primary-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
.subtext {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--tertiary-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//! FIXME: Move this to the Header component, do this:
|
||||||
|
// 1. Check if header has topic, if yes, flex-grow: 0 on the title.
|
||||||
|
// 2. If header has no topic (example: friends page), flex-grow 1 on the header title.
|
||||||
|
.title {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
90
src/pages/friends/Friend.tsx
Normal file
90
src/pages/friends/Friend.tsx
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import styles from "./Friend.module.scss";
|
||||||
|
import { useContext } from "preact/hooks";
|
||||||
|
import { Children } from "../../types/Preact";
|
||||||
|
import { X, Plus, Mail } from "@styled-icons/feather";
|
||||||
|
import UserIcon from "../../components/common/UserIcon";
|
||||||
|
import IconButton from "../../components/ui/IconButton";
|
||||||
|
import { attachContextMenu } from "preact-context-menu";
|
||||||
|
import { User, Users } from "revolt.js/dist/api/objects";
|
||||||
|
import UserStatus from '../../components/common/UserStatus';
|
||||||
|
import { stopPropagation } from "../../lib/stopPropagation";
|
||||||
|
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
||||||
|
import { useIntermediate } from "../../context/intermediate/Intermediate";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Friend({ user }: Props) {
|
||||||
|
const client = useContext(AppContext);
|
||||||
|
const { openScreen } = useIntermediate();
|
||||||
|
|
||||||
|
const actions: Children[] = [];
|
||||||
|
let subtext: Children = null;
|
||||||
|
|
||||||
|
if (user.relationship === Users.Relationship.Friend) {
|
||||||
|
subtext = <UserStatus user={user} />
|
||||||
|
actions.push(
|
||||||
|
<IconButton type="circle"
|
||||||
|
onClick={stopPropagation}>
|
||||||
|
<Link to={'/open/' + user._id}>
|
||||||
|
<Mail size={20} />
|
||||||
|
</Link>
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.relationship === Users.Relationship.Incoming) {
|
||||||
|
actions.push(
|
||||||
|
<IconButton type="circle"
|
||||||
|
onClick={ev => stopPropagation(ev, client.users.addFriend(user.username))}>
|
||||||
|
<Plus size={24} />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
subtext = <Text id="app.special.friends.incoming" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.relationship === Users.Relationship.Outgoing) {
|
||||||
|
subtext = <Text id="app.special.friends.outgoing" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
user.relationship === Users.Relationship.Friend ||
|
||||||
|
user.relationship === Users.Relationship.Outgoing ||
|
||||||
|
user.relationship === Users.Relationship.Incoming
|
||||||
|
) {
|
||||||
|
actions.push(
|
||||||
|
<IconButton type="circle"
|
||||||
|
onClick={ev => stopPropagation(ev, client.users.removeFriend(user._id))}>
|
||||||
|
<X size={24} />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.relationship === Users.Relationship.Blocked) {
|
||||||
|
actions.push(
|
||||||
|
<IconButton type="circle"
|
||||||
|
onClick={ev => stopPropagation(ev, client.users.unblockUser(user._id))}>
|
||||||
|
<X size={24} />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.friend}
|
||||||
|
onClick={() => openScreen({ id: 'profile', user_id: user._id })}
|
||||||
|
onContextMenu={attachContextMenu('Menu', { user: user._id })}>
|
||||||
|
<UserIcon target={user} size={32} status />
|
||||||
|
<div className={styles.name}>
|
||||||
|
<span>@{user.username}</span>
|
||||||
|
{subtext && (
|
||||||
|
<span className={styles.subtext}>{subtext}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.actions}>{actions}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
85
src/pages/friends/Friends.tsx
Normal file
85
src/pages/friends/Friends.tsx
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import styles from "./Friend.module.scss";
|
||||||
|
import { UserPlus } from "@styled-icons/feather";
|
||||||
|
|
||||||
|
import { Friend } from "./Friend";
|
||||||
|
import { Text } from "preact-i18n";
|
||||||
|
import Header from "../../components/ui/Header";
|
||||||
|
import Overline from "../../components/ui/Overline";
|
||||||
|
import IconButton from "../../components/ui/IconButton";
|
||||||
|
import { useUsers } from "../../context/revoltjs/hooks";
|
||||||
|
import { User, Users } from "revolt.js/dist/api/objects";
|
||||||
|
import { useIntermediate } from "../../context/intermediate/Intermediate";
|
||||||
|
|
||||||
|
export default function Friends() {
|
||||||
|
const { openScreen } = useIntermediate();
|
||||||
|
|
||||||
|
const users = useUsers() as User[];
|
||||||
|
users.sort((a, b) => a.username.localeCompare(b.username));
|
||||||
|
|
||||||
|
const pending = users.filter(
|
||||||
|
x =>
|
||||||
|
x.relationship === Users.Relationship.Incoming ||
|
||||||
|
x.relationship === Users.Relationship.Outgoing
|
||||||
|
);
|
||||||
|
const friends = users.filter(
|
||||||
|
x => x.relationship === Users.Relationship.Friend
|
||||||
|
);
|
||||||
|
const blocked = users.filter(
|
||||||
|
x => x.relationship === Users.Relationship.Blocked
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header placement="primary">
|
||||||
|
<div className={styles.title}>
|
||||||
|
<Text id="app.navigation.tabs.friends" />
|
||||||
|
</div>
|
||||||
|
<div className="actions">
|
||||||
|
<IconButton onClick={() => openScreen({ id: 'special_input', type: 'add_friend' })}>
|
||||||
|
<UserPlus size={24} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Header>
|
||||||
|
<div
|
||||||
|
className={styles.list}
|
||||||
|
data-empty={
|
||||||
|
pending.length + friends.length + blocked.length === 0
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{pending.length + friends.length + blocked.length === 0 && (
|
||||||
|
<>
|
||||||
|
<img src="https://img.insrt.uk/xexu7/XOPoBUTI47.png/raw" />
|
||||||
|
<Text id="app.special.friends.nobody" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{pending.length > 0 && (
|
||||||
|
<Overline type="subtle">
|
||||||
|
<Text id="app.special.friends.pending" /> –{" "}
|
||||||
|
{pending.length}
|
||||||
|
</Overline>
|
||||||
|
)}
|
||||||
|
{pending.map(y => (
|
||||||
|
<Friend key={y._id} user={y} />
|
||||||
|
))}
|
||||||
|
{friends.length > 0 && (
|
||||||
|
<Overline type="subtle">
|
||||||
|
<Text id="app.navigation.tabs.friends" /> –{" "}
|
||||||
|
{friends.length}
|
||||||
|
</Overline>
|
||||||
|
)}
|
||||||
|
{friends.map(y => (
|
||||||
|
<Friend key={y._id} user={y} />
|
||||||
|
))}
|
||||||
|
{blocked.length > 0 && (
|
||||||
|
<Overline type="subtle">
|
||||||
|
<Text id="app.special.friends.blocked" /> –{" "}
|
||||||
|
{blocked.length}
|
||||||
|
</Overline>
|
||||||
|
)}
|
||||||
|
{blocked.map(y => (
|
||||||
|
<Friend key={y._id} user={y} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,3 +1,8 @@
|
||||||
|
:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 3px;
|
width: 3px;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { VNode } from "preact";
|
import { VNode } from "preact";
|
||||||
|
|
||||||
export type Child = VNode | string | false | undefined;
|
export type Child = VNode | string | number | boolean | undefined | null;
|
||||||
export type Children = Child | Child[] | Children[];
|
export type Children = Child | Child[] | Children[];
|
||||||
|
|
|
@ -3038,6 +3038,13 @@ postcss@^8.3.0:
|
||||||
nanoid "^3.1.23"
|
nanoid "^3.1.23"
|
||||||
source-map-js "^0.6.2"
|
source-map-js "^0.6.2"
|
||||||
|
|
||||||
|
preact-context-menu@^0.1.5:
|
||||||
|
version "0.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/preact-context-menu/-/preact-context-menu-0.1.5.tgz#51d13b0eceed8bd53493f2bdbce36e7c74ff8575"
|
||||||
|
integrity sha512-cQxcOf4w8MZAQtLpQeX80TQ2TzSKD/z6rnV+654xiHzqQp2pSV+qE0IEZLkUNz88ZmtpIy6GnB/6pCnCGjS4Ww==
|
||||||
|
dependencies:
|
||||||
|
preact "^10.4.6"
|
||||||
|
|
||||||
preact-i18n@^2.4.0-preactx:
|
preact-i18n@^2.4.0-preactx:
|
||||||
version "2.4.0-preactx"
|
version "2.4.0-preactx"
|
||||||
resolved "https://registry.yarnpkg.com/preact-i18n/-/preact-i18n-2.4.0-preactx.tgz#fbcb2e3ae22744c7fef5a102db2ef7506057d082"
|
resolved "https://registry.yarnpkg.com/preact-i18n/-/preact-i18n-2.4.0-preactx.tgz#fbcb2e3ae22744c7fef5a102db2ef7506057d082"
|
||||||
|
@ -3051,7 +3058,7 @@ preact-markup@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/preact-markup/-/preact-markup-2.1.1.tgz#0451e7eed1dac732d7194c34a7f16ff45a2cfdd7"
|
resolved "https://registry.yarnpkg.com/preact-markup/-/preact-markup-2.1.1.tgz#0451e7eed1dac732d7194c34a7f16ff45a2cfdd7"
|
||||||
integrity sha512-8JL2p36mzK8XkspOyhBxUSPjYwMxDM0L5BWBZWxsZMVW8WsGQrYQDgVuDKkRspt2hwrle+Cxr/053hpc9BJwfw==
|
integrity sha512-8JL2p36mzK8XkspOyhBxUSPjYwMxDM0L5BWBZWxsZMVW8WsGQrYQDgVuDKkRspt2hwrle+Cxr/053hpc9BJwfw==
|
||||||
|
|
||||||
preact@^10.0.0, preact@^10.5.13:
|
preact@^10.0.0, preact@^10.4.6, preact@^10.5.13:
|
||||||
version "10.5.13"
|
version "10.5.13"
|
||||||
resolved "https://registry.yarnpkg.com/preact/-/preact-10.5.13.tgz#85f6c9197ecd736ce8e3bec044d08fd1330fa019"
|
resolved "https://registry.yarnpkg.com/preact/-/preact-10.5.13.tgz#85f6c9197ecd736ce8e3bec044d08fd1330fa019"
|
||||||
integrity sha512-q/vlKIGNwzTLu+jCcvywgGrt+H/1P/oIRSD6mV4ln3hmlC+Aa34C7yfPI4+5bzW8pONyVXYS7SvXosy2dKKtWQ==
|
integrity sha512-q/vlKIGNwzTLu+jCcvywgGrt+H/1P/oIRSD6mV4ln3hmlC+Aa34C7yfPI4+5bzW8pONyVXYS7SvXosy2dKKtWQ==
|
||||||
|
|
Loading…
Reference in a new issue