Skip to content

Commit

Permalink
feat: online users are shown on panel now
Browse files Browse the repository at this point in the history
  • Loading branch information
mslxl committed Apr 9, 2024
1 parent 1b7de54 commit 4e4c201
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 30 deletions.
81 changes: 81 additions & 0 deletions src/components/collab/connected.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import clsx from "clsx"
import { useAtomValue, useSetAtom } from "jotai"
import { ImExit } from "react-icons/im"
import { OnlineUserInfo, disconnectProviderAtom, onlineUsersAtom } from "@/store/source/provider"
import { activedSourceIdAtom, sourceAtom } from "@/store/source"
import { Avatar, AvatarFallback } from "../ui/avatar"
import { useRef } from "react"
import { useHoverDirty } from "react-use"
import { dialog } from "@tauri-apps/api"

interface UserInfoProps {
user: OnlineUserInfo
}

function UserInfo(props: UserInfoProps) {
const store = useAtomValue(sourceAtom)
const focusSourceName = store.get(props.user.activeId)?.useName()
const isIdle = (focusSourceName?.length ?? 0) == 0
const setActivedSource = useSetAtom(activedSourceIdAtom)
function changeActiveSource() {
if (!isIdle) {
setActivedSource(props.user.activeId)
}
}
return (
<li className="flex items-center space-x-4 border py-2 px-4">
<Avatar className="shadow-md">
<AvatarFallback className="font-bold text-white" style={{ background: props.user.color }}>
{props.user.name.substring(0, 1).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex-1 space-y-1">
<p className="text-sm font-medium leading-none">{props.user.name}</p>
<p
className={clsx("text-sm text-muted-foreground truncate", {
italic: isIdle,
"cursor-not-allowed": isIdle,
"cursor-pointer": !isIdle,
})}
onClick={changeActiveSource}
>
{isIdle ? "IDLE" : focusSourceName}
</p>
</div>
</li>
)
}

interface ConnectedProps {
className?: string
}

export default function Connected(props: ConnectedProps) {
const panelRef = useRef<HTMLDivElement>(null)
const disconnect = useSetAtom(disconnectProviderAtom)
const onlineUsers = useAtomValue(onlineUsersAtom)
const isHover = useHoverDirty(panelRef)

async function exitRoom(){
if(await dialog.ask('Are you sure to exit current room?', {title: 'Exit'})){
disconnect()
}
}

return (
<div className={clsx(props.className, "h-full select-none flex flex-col min-h-0 min-w-0")} ref={panelRef}>
<div className="shadow-sm flex pl-2 top-0 bg-accent">
<span className="truncate font-semibold">Online Users</span>
<span className="flex-1"></span>
<button className={clsx("p-1 hover:bg-neutral-200", {hidden: !isHover})} onClick={exitRoom}>
<ImExit />
</button>
</div>
<ul className="space-y-2 w-full p-1">
{onlineUsers.map((info) => (
<UserInfo key={info.clientId} user={info} />
))}
</ul>
</div>
)
}
3 changes: 2 additions & 1 deletion src/components/collab/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { docProviderStateAtom } from "@/store/source/provider"
import { useAtomValue } from "jotai"
import Unconnect from "./unconnect"
import Connecting from "./connecting"
import Connected from "./connected"

interface CollabPanelProps {
className?: string
Expand All @@ -17,6 +18,6 @@ export default function CollabPanel(props: CollabPanelProps) {
return <Connecting className={props.className} />
}
if (connectedStatus == "connected") {
return <>TODO: connected</>
return <Connected className={props.className}/>
}
}
2 changes: 1 addition & 1 deletion src/components/editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,5 @@ export default function Editor(props: EditorProps) {
}
}, [props.text])

return <div className={clsx("w-full", props.className, "border border-slate-400")} ref={parentRef}></div>
return <div className={clsx("w-full", props.className)} ref={parentRef}></div>
}
4 changes: 2 additions & 2 deletions src/components/runner/addition-msg.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ export default function AdditionMessage(props: AdditionMessageProps) {
return (
<div>
<span className="text-sm">Stderr:</span>
<Editor className="min-w-0 m-2" text={props.stderrLog}/>
<Editor className="min-w-0 p-2" text={props.stderrLog}/>
<span className="text-sm">Checker Report:</span>
<Editor className="min-w-0 m-2" text={props.checkReport} />
<Editor className="min-w-0 p-2" text={props.checkReport} />
</div>
)
}
6 changes: 3 additions & 3 deletions src/components/runner/single.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,11 @@ export default function SingleRunner(props: SingleRunnerProps) {
</AccordionTrigger>
<AccordionContent>
<span className="text-sm px-2">Input:</span>
<Editor className="min-w-0 m-2" text={props.testcase.input} />
<Editor className="min-w-0 p-2" text={props.testcase.input} />
<span className="text-sm px-2">Expected Output:</span>
<Editor className="min-w-0 m-2" text={props.testcase.except} />
<Editor className="min-w-0 p-2" text={props.testcase.except} />
<span className="text-sm px-2">Ouput:</span>
<Editor className="min-w-0 m-2" text={stdout} />
<Editor className="min-w-0 p-2" text={stdout} />
<Popover>
<PopoverTrigger asChild>
<span className="text-end text-xs w-full px-2 hover:text-gray-600">See Report&gt;&gt;</span>
Expand Down
8 changes: 8 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,11 @@ export function filterCSSQuote(text: string): string {
}
return result
}


export function normalizeColor(hex: string, fillColor: string = '00'): string{
const result = hex.match(/^#?((?:[\d(?:a-f)(?:A-F)]{2}){1,3})/)
if(result == null) return `#${fillColor}${fillColor}${fillColor}`
const value = result[1]
return `#${value.padEnd(6, fillColor)}`
}
5 changes: 0 additions & 5 deletions src/store/configuration.ts

This file was deleted.

11 changes: 7 additions & 4 deletions src/store/setting/form.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import {z} from 'zod'
import { atomWithSettings } from '.'
import { z } from "zod"
import { atomWithSettings } from "."

export const collabConnectFormSchema = z.object({
p2p: z.boolean(),
server: z.string().startsWith("ws://").or(z.string().startsWith("wss://")),
username: z.string().min(5).max(20),
password: z.string().min(8).max(128),
roomName: z.string().min(5).max(20),
color: z.string().regex(/([\d(a-f)(A-F)]{3}){1,2}/)
color: z.string().regex(/^([\d(a-f)(A-F)]{3}){1,2}$/, { message: "String must be a valid color" }),
})

export const lastConnectFormAtom = atomWithSettings<z.infer<typeof collabConnectFormSchema> | null>('last.connect', null)
export const lastConnectFormAtom = atomWithSettings<z.infer<typeof collabConnectFormSchema> | null>(
"last.connect",
null,
)
41 changes: 40 additions & 1 deletion src/store/source/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,55 @@
import { Doc, Array } from "yjs"
import { SourceStore } from "./model"
import { atom } from "jotai"
import { forEach, includes, isEmpty, range, uniq } from "lodash/fp"
import { clone, forEach, includes, isEmpty, range, uniq } from "lodash/fp"
import * as log from "tauri-plugin-log-api"
import { LanguageMode } from "@/lib/ipc"
import { createYjsHookAtom } from "@/lib/hooks/useY"
import generateRandomName from "@/lib/names"
import { Awareness } from "y-protocols/awareness"
import { atomWithObservable } from "jotai/utils"

export const docAtom = atom(new Doc({ autoLoad: true }))
export const awarenessAtom = atom((get) => {
return new Awareness(get(docAtom))
})

type AwarenessStates = Map<number, { [name: string]: any }>
/**
* All awareness states (remote and local).
* Maps from clientID to awareness state.
* The clientID is usually the ydoc.clientID
*/
export const awarenessStateAtom = atomWithObservable(
(get) => {
const awareness = get(awarenessAtom)

return {
subscribe(observer: { next: (data: AwarenessStates) => void }): { unsubscribe: () => void } {
const cb = () => {
const states = awareness.getStates()
observer.next(clone(states))
}
cb()
awareness.on("update", cb)
return {
unsubscribe() {
awareness.off("update", cb)
},
}
},
}
},
{ initialValue: new Map() },
)

/**
* Sync current user's focus, which used to show status on collab editing
* */
export const syncAwarenessActiveSourceIdAtom = atom(null, (get) => {
get(awarenessAtom).setLocalStateField("active", get(activedSourceIdAtom))
})

export const sourceAtom = atom((get) => new SourceStore(get(docAtom)))

/**
Expand Down Expand Up @@ -47,6 +85,7 @@ export const activedSourceIdAtom = atom<string | null, [id: string | null], void
(get, set, id) => {
if (includes(id, get(sourceIdsAtom))) {
set(activedSourceIdUncheckedAtom, id)
set(syncAwarenessActiveSourceIdAtom)
} else {
set(activedSourceIdUncheckedAtom, null)
}
Expand Down
6 changes: 0 additions & 6 deletions src/store/source/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,6 @@ export class Source {
private store: YjsNS
constructor(store: YjsNS) {
this.store = store
this.name.observe((v)=>{
console.log(v.changes)

})
}

get id(): string {
Expand Down Expand Up @@ -232,8 +228,6 @@ export class SourceStore {
delete(id: string) {
const index = this.list.toArray().indexOf(id)
if (index != -1) {
let doc = this.doc.getMap().get(id) as Doc
doc.destroy()
this.list.delete(index)
}
}
Expand Down
44 changes: 37 additions & 7 deletions src/store/source/provider.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { WebsocketProvider } from "y-websocket"
import { atom } from "jotai"
import { awarenessAtom, docAtom } from "."
import { awarenessAtom, awarenessStateAtom, docAtom, syncAwarenessActiveSourceIdAtom } from "."
import * as log from "tauri-plugin-log-api"
import { atomWithObservable } from "jotai/utils"
import { map } from "lodash/fp"
import { normalizeColor } from "@/lib/utils"

export const docProviderAtom = atom<null | WebsocketProvider>(null)

Expand Down Expand Up @@ -48,6 +50,36 @@ export interface UserAwareness {
color: string
}

export interface UserFocusDocId {
activeId: string
}

export interface UserCursor {
cursor: string
}

export type OnlineUserInfo = UserAwareness &
UserFocusDocId &
(UserFocusDocId | {}) & {
clientId: number
}

export const onlineUsersAtom = atom<OnlineUserInfo[]>((get) => {
const states = get(awarenessStateAtom)
return map(
([clientId, client]) =>
Object.assign(
client.user,
{ cursor: client?.cursor?.anchor?.tname },
{ activeId: client.active },
{
clientId: clientId,
},
),
Array.from(states.entries()),
)
})

export const connectProviderAtom = atom(
null,
(get, set, address: string, roomName: string, user?: UserAwareness, retriedTimes: number = 3) => {
Expand All @@ -63,20 +95,18 @@ export const connectProviderAtom = atom(
wsProvider.awareness.setLocalStateField(
"user",
Object.assign(user, {
colorLight: user.color + "cc",
colorLight: normalizeColor(user.color) + "cc",
}),
)
}
// logging
wsProvider.on("status", ({ status }: { status: string }) => {
log.info(`provider connection status: ${status}`)
})
set(docProviderAtom, wsProvider)

// doc.on("subdocs", ({ loaded }) => {
// loaded.forEach((subdoc) => {
// new WebsocketProvider(address, roomName + subdoc.guid, subdoc)
// })
// })
set(syncAwarenessActiveSourceIdAtom)

const promise = new Promise<string>((resolve, reject) => {
let count = 0
const callback = ({ status }: { status: string }) => {
Expand Down

0 comments on commit 4e4c201

Please sign in to comment.