mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33:41 -05:00
Lots of changes (see extended)
* Change the display of presence setter to use a list of the three statuses for accessibility, rather than the (arguably cooler) grid of three buttons * Make the status message setter fit better with the gomuks theme * added a `clear` button in addition to the status setter * Seperated the edit/display into seperate components, and put them in their own file * Added the update_presence event to the backend * Presence indicators are now customisable SVG circles, instead of emojis * some more stuff that I probably forgot
This commit is contained in:
parent
1d46df7c0f
commit
1baf28b7d9
10 changed files with 230 additions and 56 deletions
|
@ -82,3 +82,9 @@ type ClientState struct {
|
||||||
DeviceID id.DeviceID `json:"device_id,omitempty"`
|
DeviceID id.DeviceID `json:"device_id,omitempty"`
|
||||||
HomeserverURL string `json:"homeserver_url,omitempty"`
|
HomeserverURL string `json:"homeserver_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdatePresence struct {
|
||||||
|
Presence event.Presence `json:"presence"`
|
||||||
|
StatusMsg string `json:"status_msg,omitempty"`
|
||||||
|
UserID id.UserID `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
|
@ -21,6 +21,11 @@ import (
|
||||||
"go.mau.fi/gomuks/pkg/hicli/database"
|
"go.mau.fi/gomuks/pkg/hicli/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SetPresenceParams struct {
|
||||||
|
Presence event.Presence `json:"presence"`
|
||||||
|
StatusMsg string `json:"status_msg,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any, error) {
|
func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any, error) {
|
||||||
switch req.Command {
|
switch req.Command {
|
||||||
case "get_state":
|
case "get_state":
|
||||||
|
@ -92,7 +97,7 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
|
||||||
})
|
})
|
||||||
case "set_presence":
|
case "set_presence":
|
||||||
return unmarshalAndCall(req.Data, func(params *mautrix.ReqPresence) (bool, error) {
|
return unmarshalAndCall(req.Data, func(params *mautrix.ReqPresence) (bool, error) {
|
||||||
return true, h.Client.SetPresence(ctx, params)
|
return true, h.Client.SetPresence(ctx, *params)
|
||||||
})
|
})
|
||||||
case "get_mutual_rooms":
|
case "get_mutual_rooms":
|
||||||
return unmarshalAndCall(req.Data, func(params *getProfileParams) ([]id.RoomID, error) {
|
return unmarshalAndCall(req.Data, func(params *getProfileParams) ([]id.RoomID, error) {
|
||||||
|
|
|
@ -42,6 +42,8 @@ func EventTypeName(evt any) string {
|
||||||
return "send_complete"
|
return "send_complete"
|
||||||
case *ClientState:
|
case *ClientState:
|
||||||
return "client_state"
|
return "client_state"
|
||||||
|
case UpdatePresence:
|
||||||
|
return "update_presence"
|
||||||
default:
|
default:
|
||||||
panic(fmt.Errorf("unknown event type %T", evt))
|
panic(fmt.Errorf("unknown event type %T", evt))
|
||||||
}
|
}
|
||||||
|
|
|
@ -169,6 +169,20 @@ func (h *HiClient) processSyncResponse(ctx context.Context, resp *mautrix.RespSy
|
||||||
return fmt.Errorf("failed to process left room %s: %w", roomID, err)
|
return fmt.Errorf("failed to process left room %s: %w", roomID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, presenceEvent := range resp.Presence.Events {
|
||||||
|
presenceEvent.Type.Class = event.EphemeralEventPresence.Class
|
||||||
|
if presenceEvent.Content.ParseRaw(event.EphemeralEventPresence) != nil {
|
||||||
|
zerolog.Ctx(ctx).Warn().Stringer("event_id", presenceEvent.ID).Msg(fmt.Sprintf("Failed to parse presence event content: %s", presenceEvent.Content.VeryRaw))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
body := presenceEvent.Content.AsPresence()
|
||||||
|
presence := UpdatePresence{
|
||||||
|
Presence: body.Presence,
|
||||||
|
StatusMsg: body.StatusMessage,
|
||||||
|
UserID: presenceEvent.Sender,
|
||||||
|
}
|
||||||
|
h.EventHandler(presence)
|
||||||
|
}
|
||||||
h.Account.NextBatch = resp.NextBatch
|
h.Account.NextBatch = resp.NextBatch
|
||||||
err = h.DB.Account.PutNextBatch(ctx, h.Account.UserID, resp.NextBatch)
|
err = h.DB.Account.PutNextBatch(ctx, h.Account.UserID, resp.NextBatch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -186,7 +186,7 @@ export default abstract class RPCClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
setPresence(presence: Presence): Promise<boolean> {
|
setPresence(presence: Presence): Promise<boolean> {
|
||||||
return this.request("set_presence", { presence })
|
return this.request("set_presence", presence)
|
||||||
}
|
}
|
||||||
|
|
||||||
getMutualRooms(user_id: UserID): Promise<RoomID[]> {
|
getMutualRooms(user_id: UserID): Promise<RoomID[]> {
|
||||||
|
|
|
@ -46,6 +46,16 @@ export interface TypingEvent extends BaseRPCCommand<TypingEventData> {
|
||||||
command: "typing"
|
command: "typing"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdatePresenceEventData {
|
||||||
|
user_id: UserID
|
||||||
|
presence: "online" | "offline" | "unavailable"
|
||||||
|
status_msg?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdatePresenceEvent extends BaseRPCCommand<UpdatePresenceEventData> {
|
||||||
|
command: "update_presence"
|
||||||
|
}
|
||||||
|
|
||||||
export interface SendCompleteData {
|
export interface SendCompleteData {
|
||||||
event: RawDBEvent
|
event: RawDBEvent
|
||||||
error: string | null
|
error: string | null
|
||||||
|
@ -155,6 +165,7 @@ export type RPCEvent =
|
||||||
SyncCompleteEvent |
|
SyncCompleteEvent |
|
||||||
ImageAuthTokenEvent |
|
ImageAuthTokenEvent |
|
||||||
InitCompleteEvent |
|
InitCompleteEvent |
|
||||||
RunIDEvent
|
RunIDEvent |
|
||||||
|
UpdatePresenceEvent
|
||||||
|
|
||||||
export type RPCCommand = RPCEvent | ResponseCommand | ErrorCommand
|
export type RPCCommand = RPCEvent | ResponseCommand | ErrorCommand
|
||||||
|
|
|
@ -84,6 +84,10 @@
|
||||||
--timeline-horizontal-padding: 1.5rem;
|
--timeline-horizontal-padding: 1.5rem;
|
||||||
--timeline-status-size: 4rem;
|
--timeline-status-size: 4rem;
|
||||||
|
|
||||||
|
--presence-online: green;
|
||||||
|
--presence-offline: grey;
|
||||||
|
--presence-unavailable: red;
|
||||||
|
|
||||||
@media screen and (max-width: 45rem) {
|
@media screen and (max-width: 45rem) {
|
||||||
--timeline-horizontal-padding: .5rem;
|
--timeline-horizontal-padding: .5rem;
|
||||||
--timeline-status-size: 2.25rem;
|
--timeline-status-size: 2.25rem;
|
||||||
|
|
|
@ -86,7 +86,22 @@ div.right-panel-content.user {
|
||||||
div.presence {
|
div.presence {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1.125rem;
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.presence-indicator {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
border-radius: 50%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
svg.presence-online {
|
||||||
|
fill: var(--presence-online);
|
||||||
|
}
|
||||||
|
svg.presence-offline {
|
||||||
|
fill: var(--presence-offline);
|
||||||
|
}
|
||||||
|
svg.presence-unavailable {
|
||||||
|
fill: var(--presence-unavailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
div.statusmessage {
|
div.statusmessage {
|
||||||
|
@ -99,7 +114,49 @@ div.right-panel-content.user {
|
||||||
|
|
||||||
div.presencesetter {
|
div.presencesetter {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: repeat(3, 1fr);
|
||||||
|
justify-content: left;
|
||||||
|
justify-items: left;
|
||||||
|
gap: .5rem;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
div.presencesetter > button {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
padding: .5rem 0;
|
||||||
|
justify-content: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.statussetter > form {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
form.canclear {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
form.cannotclear {
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
|
||||||
|
& input[type="submit"] {
|
||||||
|
grid-column: 1 / span 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.statussetter > form > input[type="text"] {
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
border: none;
|
||||||
|
transition: border-bottom .2s;
|
||||||
|
border-bottom: 1px solid var(--blockquote-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.statussetter > form > input[type="text"]:focus {
|
||||||
|
border-bottom: 1px solid var(--primary-color);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.statussetter > form > input[type="submit"]:hover {
|
||||||
|
background-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
div.userid, div.displayname {
|
div.userid, div.displayname {
|
||||||
|
|
|
@ -26,17 +26,12 @@ import DeviceList from "./UserInfoDeviceList.tsx"
|
||||||
import UserInfoError from "./UserInfoError.tsx"
|
import UserInfoError from "./UserInfoError.tsx"
|
||||||
import MutualRooms from "./UserInfoMutualRooms.tsx"
|
import MutualRooms from "./UserInfoMutualRooms.tsx"
|
||||||
import { ErrorResponse } from "@/api/rpc.ts"
|
import { ErrorResponse } from "@/api/rpc.ts"
|
||||||
|
import { UserPresence } from "./UserPresence.tsx"
|
||||||
|
|
||||||
interface UserInfoProps {
|
interface UserInfoProps {
|
||||||
userID: UserID
|
userID: UserID
|
||||||
}
|
}
|
||||||
|
|
||||||
const PresenceEmojis = {
|
|
||||||
online: "🟢",
|
|
||||||
offline: "⚫",
|
|
||||||
unavailable: "🔴",
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserInfo = ({ userID }: UserInfoProps) => {
|
const UserInfo = ({ userID }: UserInfoProps) => {
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
const roomCtx = use(RoomContext)
|
const roomCtx = use(RoomContext)
|
||||||
|
@ -44,7 +39,6 @@ const UserInfo = ({ userID }: UserInfoProps) => {
|
||||||
const memberEvt = useRoomMember(client, roomCtx?.store, userID)
|
const memberEvt = useRoomMember(client, roomCtx?.store, userID)
|
||||||
const member = (memberEvt?.content ?? null) as MemberEventContent | null
|
const member = (memberEvt?.content ?? null) as MemberEventContent | null
|
||||||
const [globalProfile, setGlobalProfile] = useState<UserProfile | null>(null)
|
const [globalProfile, setGlobalProfile] = useState<UserProfile | null>(null)
|
||||||
const [presence, setPresence] = useState<Presence | null>(null)
|
|
||||||
const [errors, setErrors] = useState<string[] | null>(null)
|
const [errors, setErrors] = useState<string[] | null>(null)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setErrors(null)
|
setErrors(null)
|
||||||
|
@ -53,28 +47,8 @@ const UserInfo = ({ userID }: UserInfoProps) => {
|
||||||
setGlobalProfile,
|
setGlobalProfile,
|
||||||
err => setErrors([`${err}`]),
|
err => setErrors([`${err}`]),
|
||||||
)
|
)
|
||||||
client.rpc.getPresence(userID).then(
|
|
||||||
setPresence,
|
|
||||||
err => {
|
|
||||||
// A 404 is to be expected if the user has not federated presence.
|
|
||||||
if (err instanceof ErrorResponse && err.message.startsWith("M_NOT_FOUND")) {
|
|
||||||
setPresence(null)
|
|
||||||
} else {
|
|
||||||
if(errors) {setErrors([...errors, `${err}`])}
|
|
||||||
else {setErrors([`${err}`])}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}, [roomCtx, userID, client])
|
}, [roomCtx, userID, client])
|
||||||
|
|
||||||
const sendNewPresence = (newPresence: Presence) => {
|
|
||||||
console.log("Setting new presence", newPresence)
|
|
||||||
client.rpc.setPresence(newPresence).then(
|
|
||||||
() => setPresence(newPresence),
|
|
||||||
err => setErrors((errors && [...errors, `${err}`] || [`${err}`])),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayname = member?.displayname || globalProfile?.displayname || getLocalpart(userID)
|
const displayname = member?.displayname || globalProfile?.displayname || getLocalpart(userID)
|
||||||
return <>
|
return <>
|
||||||
<div className="avatar-container">
|
<div className="avatar-container">
|
||||||
|
@ -91,17 +65,7 @@ const UserInfo = ({ userID }: UserInfoProps) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="displayname" title={displayname}>{displayname}</div>
|
<div className="displayname" title={displayname}>{displayname}</div>
|
||||||
<div className="userid" title={userID}>{userID}</div>
|
<div className="userid" title={userID}>{userID}</div>
|
||||||
{presence && (
|
<UserPresence client={client} userID={userID}/>
|
||||||
<>
|
|
||||||
<div className="presence" title={presence.presence}>{PresenceEmojis[presence.presence]} {presence.presence}</div>
|
|
||||||
{
|
|
||||||
presence.status_msg && (
|
|
||||||
<div className="statusmessage" title={"Status message"}><blockquote>{presence.status_msg}</blockquote></div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<hr/>
|
<hr/>
|
||||||
{userID !== client.userID && <>
|
{userID !== client.userID && <>
|
||||||
<MutualRooms client={client} userID={userID}/>
|
<MutualRooms client={client} userID={userID}/>
|
||||||
|
@ -109,20 +73,6 @@ const UserInfo = ({ userID }: UserInfoProps) => {
|
||||||
</>}
|
</>}
|
||||||
<DeviceList client={client} room={roomCtx?.store} userID={userID}/>
|
<DeviceList client={client} room={roomCtx?.store} userID={userID}/>
|
||||||
<hr/>
|
<hr/>
|
||||||
{userID === client.userID && <>
|
|
||||||
<h3>Set presence</h3>
|
|
||||||
<div className="presencesetter">
|
|
||||||
<button title="Set presence to online" onClick={() => sendNewPresence({...(presence || {}), "presence": "online"})} type="button">{PresenceEmojis["online"]}</button>
|
|
||||||
<button title="Set presence to unavailable" onClick={() => sendNewPresence({...(presence || {}), "presence": "unavailable"})} type="button">{PresenceEmojis["unavailable"]}</button>
|
|
||||||
<button title="Set presence to offline" onClick={() => sendNewPresence({...(presence || {}), "presence": "offline"})} type="button">{PresenceEmojis["offline"]}</button>
|
|
||||||
</div>
|
|
||||||
<div className="statussetter">
|
|
||||||
<form onSubmit={(e) => {e.preventDefault(); sendNewPresence({...(presence || {"presence": "offline"}), "status_msg": ((e.currentTarget as HTMLFormElement).children[0] as HTMLInputElement).value})}}>
|
|
||||||
<input type="text" placeholder="Status message" defaultValue={presence?.status_msg || ""}/><button title="Set status message" onClick={() => alert("Set status message")} type="submit">Set</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<hr/>
|
|
||||||
</>}
|
|
||||||
{errors?.length ? <>
|
{errors?.length ? <>
|
||||||
<UserInfoError errors={errors}/>
|
<UserInfoError errors={errors}/>
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
125
web/src/ui/rightpanel/UserPresence.tsx
Normal file
125
web/src/ui/rightpanel/UserPresence.tsx
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
// gomuks - A Matrix client written in Go.
|
||||||
|
// Copyright (C) 2024 Tulir Asokan
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
import { MouseEvent, useEffect, useState } from "react"
|
||||||
|
import { UserID, Presence, RPCEvent } from "@/api/types"
|
||||||
|
import { ErrorResponse } from "@/api/rpc.ts"
|
||||||
|
import Client from "@/api/client"
|
||||||
|
|
||||||
|
interface UserPresenceProps {
|
||||||
|
client: Client,
|
||||||
|
userID: UserID
|
||||||
|
}
|
||||||
|
interface EditUserPresenceProps {
|
||||||
|
client: Client,
|
||||||
|
presence: Presence,
|
||||||
|
setter: (presence: Presence) => void
|
||||||
|
}
|
||||||
|
const PresenceEmojis = {
|
||||||
|
"online": <svg className="presence-indicator presence-online" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><circle cx="16" cy="16" r="50" /></svg>,
|
||||||
|
"offline": <svg className="presence-indicator presence-offline" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><circle cx="16" cy="16" r="50" /></svg>,
|
||||||
|
"unavailable": <svg className="presence-indicator presence-unavailable" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><circle cx="16" cy="16" r="50" /></svg>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserPresence = ({ client, userID }: UserPresenceProps) => {
|
||||||
|
const [presence, setPresence] = useState<Presence | null>(null)
|
||||||
|
const [errors, setErrors] = useState<string[] | null>(null)
|
||||||
|
|
||||||
|
client.rpc.event.listen((event: RPCEvent) => {
|
||||||
|
if (event.command === "update_presence" && event.data.user_id === userID) {
|
||||||
|
setPresence({
|
||||||
|
"presence": event.data.presence,
|
||||||
|
"status_msg": event.data.status_msg || null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
client.rpc.getPresence(userID).then(
|
||||||
|
setPresence,
|
||||||
|
err => {
|
||||||
|
// A 404 is to be expected if the user has not federated presence.
|
||||||
|
if (err instanceof ErrorResponse && err.message.startsWith("M_NOT_FOUND")) {
|
||||||
|
setPresence(null)
|
||||||
|
} else {
|
||||||
|
errors?.length ? setErrors([...errors, `${err}`]) : setErrors([`${err}`])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}, [client, userID])
|
||||||
|
|
||||||
|
if(!presence) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DisplayUserPresence presence={presence} />
|
||||||
|
{
|
||||||
|
userID === client.userID && <EditUserPresence client={client} presence={presence} setter={setPresence}/>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisplayUserPresence = ({ presence }: { presence: Presence | null }) => {
|
||||||
|
if(!presence) return null
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="presence" title={presence.presence}>{PresenceEmojis[presence.presence]} {presence.presence}</div>
|
||||||
|
{
|
||||||
|
presence.status_msg && (
|
||||||
|
<div className="statusmessage" title={"Status message"}><blockquote>{presence.status_msg}</blockquote></div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditUserPresence = ({ client, presence, setter }: EditUserPresenceProps) => {
|
||||||
|
const sendNewPresence = (newPresence: Presence) => {
|
||||||
|
client.rpc.setPresence(newPresence).then(
|
||||||
|
() => setter(newPresence),
|
||||||
|
err => console.error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const clearStatusMessage = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
let p = presence || {"presence": "offline"}
|
||||||
|
if(p.status_msg) {
|
||||||
|
delete p.status_msg
|
||||||
|
}
|
||||||
|
const textInputElement = e.currentTarget.parentElement?.querySelector("input[type=text]") as HTMLInputElement
|
||||||
|
client.rpc.setPresence(p).then(
|
||||||
|
() => {setter(p); if(textInputElement) textInputElement.value = ""},
|
||||||
|
err => console.error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4>Set presence</h4>
|
||||||
|
<div className="presencesetter">
|
||||||
|
<button title="Set presence to online" onClick={() => sendNewPresence({...(presence || {}), "presence": "online"})} type="button">{PresenceEmojis["online"]} Online</button>
|
||||||
|
<button title="Set presence to unavailable" onClick={() => sendNewPresence({...(presence || {}), "presence": "unavailable"})} type="button">{PresenceEmojis["unavailable"]} Unavailable</button>
|
||||||
|
<button title="Set presence to offline" onClick={() => sendNewPresence({...(presence || {}), "presence": "offline"})} type="button">{PresenceEmojis["offline"]} Offline</button>
|
||||||
|
</div>
|
||||||
|
<p></p>
|
||||||
|
<div className="statussetter">
|
||||||
|
<form className={presence?.status_msg ? "canclear" : "cannotclear"} onSubmit={(e) => {e.preventDefault(); sendNewPresence({...(presence || {}), "status_msg": (e.currentTarget[0] as HTMLInputElement).value})}}>
|
||||||
|
<input type="text" placeholder="Status message" defaultValue={presence?.status_msg || ""}/>
|
||||||
|
{presence?.status_msg && <button title="Clear status" type="button" onClick={(e) => clearStatusMessage(e)}>Clear</button>}
|
||||||
|
<button title="Set status message" type="submit">Set</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue