Skip to content
GitLab
Explore
Sign in
Register
Hide whitespace changes
Inline
Side-by-side
packages/client/src/components/Profile/UserCard.tsx
0 → 100644
View file @
51eacdaf
import
{
faMessage
,
faWarning
}
from
"
@fortawesome/free-solid-svg-icons
"
;
import
{
FontAwesomeIcon
}
from
"
@fortawesome/react-fontawesome
"
;
import
{
Button
,
Link
,
Spinner
,
User
}
from
"
@nextui-org/react
"
;
import
{
ClientConfig
}
from
"
@sc07-canvas/lib/src/net
"
;
import
{
MouseEvent
,
useEffect
,
useMemo
,
useState
}
from
"
react
"
;
import
{
toast
}
from
"
react-toastify
"
;
import
{
useAppContext
}
from
"
../../contexts/AppContext
"
;
export
interface
IUser
{
sub
:
string
;
display_name
?:
string
;
picture_url
?:
string
;
profile_url
?:
string
;
isAdmin
:
boolean
;
isModerator
:
boolean
;
}
const
getMatrixLink
=
(
user
:
IUser
,
config
:
ClientConfig
)
=>
{
return
`
${
config
.
chat
.
element_host
}
/#/user/@
${
user
.
sub
.
replace
(
"
@
"
,
"
=40
"
)}
:
${
config
.
chat
.
matrix_homeserver
}
`
;
};
/**
* Small UserCard that shows profile picture, display name, full username, message box and (currently unused) view profile button
* @param param0
* @returns
*/
export
const
UserCard
=
({
user
}:
{
user
:
IUser
})
=>
{
const
{
config
,
setProfile
}
=
useAppContext
();
const
[
messageStatus
,
setMessageStatus
]
=
useState
<
"
loading
"
|
"
no_account
"
|
"
has_account
"
|
"
error
"
>
(
"
loading
"
);
useEffect
(()
=>
{
if
(
!
config
)
{
console
.
warn
(
"
[UserCard] config is not available yet
"
);
return
;
}
setMessageStatus
(
"
loading
"
);
fetch
(
`https://
${
config
.
chat
.
matrix_homeserver
}
/_matrix/client/v3/profile/
${
encodeURIComponent
(
`@
${
user
.
sub
.
replace
(
"
@
"
,
"
=40
"
)}
:
${
config
.
chat
.
matrix_homeserver
}
`
)}
`
)
.
then
((
req
)
=>
{
if
(
req
.
status
===
200
)
{
setMessageStatus
(
"
has_account
"
);
}
else
{
setMessageStatus
(
"
no_account
"
);
}
})
.
catch
((
e
)
=>
{
console
.
error
(
"
Error while getting Matrix account details for
"
+
user
.
sub
,
e
);
setMessageStatus
(
"
error
"
);
toast
.
error
(
"
Error while getting Matrix account details for
"
+
user
.
sub
);
});
},
[
user
,
config
]);
const
handleMatrixClick
=
(
e
:
MouseEvent
)
=>
{
if
(
messageStatus
===
"
no_account
"
)
{
e
.
preventDefault
();
toast
.
info
(
"
This user has not setup chat yet, you cannot message them
"
);
}
};
const
openProfile
=
()
=>
{
setProfile
(
user
.
sub
);
};
const
name
=
useMemo
(()
=>
{
if
(
!
user
||
!
user
.
sub
)
{
return
"
Unknown
"
;
}
const
regex
=
/^
(
.*
)
@/
;
const
match
=
user
.
sub
.
match
(
regex
);
if
(
match
)
{
return
match
[
1
];
}
return
"
Unknown
"
;
},
[
user
]);
return
(
<
div
className
=
"flex flex-col gap-1"
>
<
div
className
=
"flex flex-row space-between p-2"
>
<
User
name
=
{
user
?.
display_name
||
name
}
description
=
{
user
?.
sub
||
"
Unknown
"
}
avatarProps
=
{
{
showFallback
:
true
,
name
:
undefined
,
src
:
user
?.
picture_url
,
}
}
/>
<
div
className
=
"ml-auto"
>
{
config
&&
(
<
Button
isIconOnly
as
=
{
Link
}
href
=
{
getMatrixLink
(
user
,
config
)
}
target
=
"_blank"
onClick
=
{
handleMatrixClick
}
>
{
messageStatus
===
"
loading
"
?
(
<
Spinner
/>
)
:
(
<
FontAwesomeIcon
icon
=
{
messageStatus
===
"
error
"
?
faWarning
:
faMessage
}
color
=
"inherit"
/>
)
}
</
Button
>
)
}
</
div
>
</
div
>
<
Button
size
=
"sm"
onPress
=
{
openProfile
}
>
View Profile
</
Button
>
</
div
>
);
};
packages/client/src/components/Settings/ChatSettings.tsx
0 → 100644
View file @
51eacdaf
import
{
Switch
}
from
"
@nextui-org/react
"
;
import
{
useAppContext
}
from
"
../../contexts/AppContext
"
;
import
React
,
{
lazy
}
from
"
react
"
;
const
InnerChatSettings
=
lazy
(()
=>
import
(
"
../Chat/InnerChatSettings
"
));
export
const
ChatSettings
=
()
=>
{
const
{
loadChat
,
setLoadChat
}
=
useAppContext
();
return
(
<
div
className
=
"flex flex-col p-2"
>
<
header
className
=
"flex flex-col gap-2"
>
<
div
className
=
"flex items-center"
>
<
Switch
size
=
"sm"
isSelected
=
{
loadChat
||
false
}
onValueChange
=
{
setLoadChat
}
/>
<
h2
className
=
"text-xl"
>
Chat
</
h2
>
</
div
>
<
p
className
=
"text-default-600 text-xs"
>
Chatting with other canvas users
</
p
>
</
header
>
<
section
>
<
React
.
Suspense
>
{
loadChat
&&
<
div
className
=
"mt-4"
>
<
InnerChatSettings
/>
</
div
>
}
</
React
.
Suspense
>
</
section
>
</
div
>
);
};
packages/client/src/components/Settings/SettingsSidebar.tsx
0 → 100644
View file @
51eacdaf
import
{
faGear
}
from
"
@fortawesome/free-solid-svg-icons
"
;
import
{
useAppContext
}
from
"
../../contexts/AppContext
"
;
import
{
SidebarBase
}
from
"
../SidebarBase
"
;
import
{
Button
,
Divider
}
from
"
@nextui-org/react
"
;
import
{
TemplateSettings
}
from
"
./TemplateSettings
"
;
import
{
ChatSettings
}
from
"
./ChatSettings
"
;
import
{
OverlaySettings
}
from
"
../Overlay/OverlaySettings
"
;
export
const
SettingsSidebar
=
()
=>
{
const
{
settingsSidebar
,
setSettingsSidebar
,
setShowKeybinds
}
=
useAppContext
();
return
(
<
SidebarBase
shown
=
{
settingsSidebar
}
setSidebarShown
=
{
setSettingsSidebar
}
icon
=
{
faGear
}
title
=
"Settings"
description
=
"Configuration options for customizing your experience"
side
=
"Right"
>
<
div
className
=
"p-4 flex flex-col gap-4"
>
<
TemplateSettings
/>
<
Divider
/>
<
ChatSettings
/>
<
Divider
/>
<
OverlaySettings
/>
<
Divider
/>
<
section
>
<
Button
onPress
=
{
()
=>
{
setShowKeybinds
(
true
);
setSettingsSidebar
(
false
);
}
}
>
Keybinds
</
Button
>
</
section
>
</
div
>
</
SidebarBase
>
)
};
\ No newline at end of file
packages/client/src/components/Settings/TemplateSettings.tsx
0 → 100644
View file @
51eacdaf
import
{
useTemplateContext
}
from
"
../../contexts/TemplateContext
"
;
import
{
Input
,
Select
,
SelectItem
,
Slider
,
Switch
}
from
"
@nextui-org/react
"
;
export
const
TemplateSettings
=
()
=>
{
const
{
enable
,
setEnable
,
url
,
setURL
,
width
,
setWidth
,
x
,
setX
,
y
,
setY
,
opacity
,
setOpacity
,
style
,
setStyle
,
showMobileTools
,
setShowMobileTools
,
}
=
useTemplateContext
();
return
(
<
div
className
=
"flex flex-col p-2"
>
<
header
className
=
"flex flex-col gap-2"
>
<
div
className
=
"flex items-center"
>
<
Switch
size
=
"sm"
isSelected
=
{
enable
||
false
}
onValueChange
=
{
setEnable
}
/>
<
h2
className
=
"text-xl"
>
Template
</
h2
>
</
div
>
<
p
className
=
"text-default-600 text-xs"
>
Displaying an image over the canvas to help guide placing
</
p
>
</
header
>
{
enable
&&
<
section
className
=
"flex flex-col gap-2 mt-4"
>
<
Input
label
=
"Template URL"
size
=
"sm"
value
=
{
url
||
""
}
onValueChange
=
{
setURL
}
/>
<
Input
label
=
"Template Width"
size
=
"sm"
type
=
"number"
min
=
"1"
max
=
{
10
_000
}
value
=
{
width
?.
toString
()
||
""
}
onValueChange
=
{
(
v
)
=>
setWidth
(
parseInt
(
v
))
}
/>
<
div
className
=
"flex flex-row gap-1"
>
<
Input
label
=
"Template X"
size
=
"sm"
type
=
"number"
value
=
{
x
?.
toString
()
||
""
}
onValueChange
=
{
(
v
)
=>
setX
(
parseInt
(
v
))
}
/>
<
Input
label
=
"Template Y"
size
=
"sm"
type
=
"number"
value
=
{
y
?.
toString
()
||
""
}
onValueChange
=
{
(
v
)
=>
setY
(
parseInt
(
v
))
}
/>
</
div
>
<
Slider
label
=
"Template Opacity"
step
=
{
1
}
minValue
=
{
0
}
maxValue
=
{
100
}
value
=
{
opacity
??
100
}
onChange
=
{
(
v
)
=>
setOpacity
(
v
as
number
)
}
getValue
=
{
(
v
)
=>
v
+
"
%
"
}
/>
<
Select
label
=
"Template Style"
size
=
"sm"
selectedKeys
=
{
[
style
]
}
onChange
=
{
(
e
)
=>
setStyle
(
e
.
target
.
value
as
any
)
}
>
<
SelectItem
key
=
"SOURCE"
>
Source
</
SelectItem
>
<
SelectItem
key
=
"ONE_TO_ONE"
>
One-to-one
</
SelectItem
>
<
SelectItem
key
=
"ONE_TO_ONE_INCORRECT"
>
One-to-one (keep incorrect)
</
SelectItem
>
<
SelectItem
key
=
"DOTTED_SMALL"
>
Dotted Small
</
SelectItem
>
<
SelectItem
key
=
"DOTTED_BIG"
>
Dotted Big
</
SelectItem
>
<
SelectItem
key
=
"SYMBOLS"
>
Symbols
</
SelectItem
>
<
SelectItem
key
=
"NUMBERS"
>
Numbers
</
SelectItem
>
</
Select
>
{
style
!==
"
ONE_TO_ONE
"
&&
(
<
div
>
<
b
>
Warning:
</
b
>
Template color picking only
<
br
/>
works with one-to-one template style
</
div
>
)
}
<
Switch
className
=
"md:hidden"
isSelected
=
{
showMobileTools
}
onValueChange
=
{
setShowMobileTools
}
>
Show Mobile Tools
</
Switch
>
</
section
>
}
</
div
>
);
};
packages/client/src/components/SidebarBase.tsx
0 → 100644
View file @
51eacdaf
import
{
motion
}
from
"
framer-motion
"
;
import
{
FontAwesomeIcon
}
from
"
@fortawesome/react-fontawesome
"
;
import
{
IconProp
}
from
"
@fortawesome/fontawesome-svg-core
"
;
import
{
Button
,
Divider
}
from
"
@nextui-org/react
"
;
import
{
faXmark
}
from
"
@fortawesome/free-solid-svg-icons
"
;
import
{
JSX
}
from
"
react
"
;
/**
* Information sidebar
*
* TODO: add customization for this post-event (#46)
*
* @returns
*/
export
const
SidebarBase
=
({
children
,
shown
,
icon
,
setSidebarShown
,
title
,
description
,
side
,
}:
{
children
:
string
|
JSX
.
Element
|
JSX
.
Element
[];
icon
:
IconProp
;
shown
:
boolean
;
setSidebarShown
:
(
value
:
boolean
)
=>
void
;
title
:
string
;
description
:
string
;
side
:
"
Left
"
|
"
Right
"
;
})
=>
{
return
(
<
div
>
<
motion
.
div
className
=
{
`absolute w-screen h-screen z-50 left-0 top-0 bg-black pointer-events-none`
}
initial
=
{
{
opacity
:
0
,
visibility
:
"
hidden
"
}
}
animate
=
{
{
opacity
:
shown
?
0.25
:
0
,
visibility
:
shown
?
"
visible
"
:
"
hidden
"
,
}
}
transition
=
{
{
type
:
"
spring
"
,
stiffness
:
50
}
}
/>
<
motion
.
div
className
=
{
`min-w-[20rem] max-w-[75vw] md:max-w-[30vw] bg-white dark:bg-black flex flex-col justify-between fixed
${
side
===
"
Left
"
?
"
left-0
"
:
"
right-0
"
}
h-full shadow-xl overflow-y-auto z-50 top-0`
}
initial
=
{
{
x
:
side
===
"
Left
"
?
"
-150%
"
:
"
150%
"
}
}
animate
=
{
{
x
:
shown
?
side
===
"
Left
"
?
"
-50%
"
:
"
50%
"
:
side
===
"
Left
"
?
"
-150%
"
:
"
150%
"
,
}
}
transition
=
{
{
type
:
"
spring
"
,
stiffness
:
50
}
}
/>
<
motion
.
div
className
=
{
`min-w-[20rem] max-w-[75vw] md:max-w-[30vw] bg-white dark:bg-black text-black dark:text-white flex flex-col fixed
${
side
===
"
Left
"
?
"
left-0
"
:
"
right-0
"
}
h-full shadow-xl overflow-y-auto z-50 top-0`
}
initial
=
{
{
x
:
side
===
"
Left
"
?
"
-100%
"
:
"
100%
"
}
}
animate
=
{
{
x
:
shown
?
0
:
side
===
"
Left
"
?
"
-100%
"
:
"
100%
"
}
}
transition
=
{
{
type
:
"
spring
"
,
stiffness
:
50
}
}
>
<
header
className
=
"flex p-4 justify-between items-center"
>
<
div
>
<
div
className
=
"flex items-center gap-2"
>
<
FontAwesomeIcon
icon
=
{
icon
}
size
=
"lg"
/>
<
div
>
<
h1
className
=
"text-xl"
>
{
title
}
</
h1
>
<
p
className
=
"text-xs text-default-600"
>
{
description
}
</
p
>
</
div
>
</
div
>
</
div
>
<
Button
size
=
"sm"
isIconOnly
onClick
=
{
()
=>
setSidebarShown
(
false
)
}
variant
=
"solid"
className
=
"ml-4"
>
<
FontAwesomeIcon
icon
=
{
faXmark
}
/>
</
Button
>
</
header
>
<
Divider
/>
{
children
}
</
motion
.
div
>
</
div
>
);
};
packages/client/src/components/Templating/MobileTemplateButtons.tsx
0 → 100644
View file @
51eacdaf
import
{
Switch
}
from
"
@nextui-org/react
"
;
import
{
useTemplateContext
}
from
"
../../contexts/TemplateContext
"
;
export
const
MobileTemplateButtons
=
()
=>
{
const
{
enable
,
setEnable
,
url
}
=
useTemplateContext
();
return
(
<
div
className
=
"md:hidden toolbar-box top-[-10px] right-[10px]"
>
{
url
&&
(
<
div
className
=
"md:hidden rounded-xl bg-gray-300 p-2"
>
<
Switch
isSelected
=
{
enable
}
onValueChange
=
{
setEnable
}
>
Template
</
Switch
>
</
div
>
)
}
</
div
>
);
};
packages/client/src/components/Templating/Template.scss
0 → 100644
View file @
51eacdaf
#template
{
width
:
100px
;
>
*
{
width
:
100%
;
// height: 100%;
position
:
absolute
;
top
:
0
;
left
:
0
;
}
}
packages/client/src/components/Templating/Template.tsx
0 → 100644
View file @
51eacdaf
import
{
useEffect
,
useRef
}
from
"
react
"
;
import
{
Template
as
TemplateCl
}
from
"
../../lib/template
"
;
import
{
useAppContext
}
from
"
../../contexts/AppContext
"
;
import
{
useTemplateContext
}
from
"
../../contexts/TemplateContext
"
;
import
{
Canvas
}
from
"
../../lib/canvas
"
;
export
const
Template
=
()
=>
{
const
{
config
}
=
useAppContext
();
const
{
enable
,
url
,
width
,
setWidth
,
x
,
y
,
opacity
,
setX
,
setY
,
style
}
=
useTemplateContext
();
const
templateHolder
=
useRef
<
HTMLDivElement
>
(
null
);
const
instance
=
useRef
<
TemplateCl
>
();
useEffect
(()
=>
{
if
(
!
templateHolder
?.
current
)
{
console
.
warn
(
"
No templateHolder, cannot initialize
"
);
return
;
}
const
templateHolderRef
=
templateHolder
.
current
;
instance
.
current
=
new
TemplateCl
(
config
!
,
templateHolder
.
current
);
instance
.
current
.
on
(
"
autoDetectWidth
"
,
(
width
)
=>
{
setWidth
(
width
);
});
let
startLocation
:
{
clientX
:
number
;
clientY
:
number
}
|
undefined
;
let
offset
:
[
x
:
number
,
y
:
number
]
=
[
0
,
0
];
const
handleMouseDown
=
(
e
:
MouseEvent
)
=>
{
if
(
!
e
.
altKey
)
return
;
startLocation
=
{
clientX
:
e
.
clientX
,
clientY
:
e
.
clientY
};
offset
=
[
e
.
offsetX
,
e
.
offsetY
];
Canvas
.
instance
?.
getPanZoom
().
panning
.
setEnabled
(
false
);
};
const
handleMouseMove
=
(
e
:
MouseEvent
)
=>
{
if
(
!
startLocation
)
return
;
if
(
!
Canvas
.
instance
)
{
console
.
warn
(
"
[Template#handleMouseMove] Canvas.instance is not defined
"
);
return
;
}
const
deltaX
=
e
.
clientX
-
startLocation
.
clientX
;
const
deltaY
=
e
.
clientY
-
startLocation
.
clientY
;
const
newX
=
startLocation
.
clientX
+
deltaX
;
const
newY
=
startLocation
.
clientY
+
deltaY
;
const
[
canvasX
,
canvasY
]
=
Canvas
.
instance
.
screenToPos
(
newX
,
newY
);
templateHolderRef
.
style
.
setProperty
(
"
left
"
,
canvasX
-
offset
[
0
]
+
"
px
"
);
templateHolderRef
.
style
.
setProperty
(
"
top
"
,
canvasY
-
offset
[
1
]
+
"
px
"
);
};
const
handleMouseUp
=
(
_e
:
MouseEvent
)
=>
{
startLocation
=
undefined
;
Canvas
.
instance
?.
getPanZoom
().
panning
.
setEnabled
(
true
);
const
x
=
parseInt
(
templateHolderRef
.
style
.
getPropertyValue
(
"
left
"
).
replace
(
"
px
"
,
""
)
||
"
0
"
);
const
y
=
parseInt
(
templateHolderRef
.
style
.
getPropertyValue
(
"
top
"
).
replace
(
"
px
"
,
""
)
||
"
0
"
);
setX
(
x
);
setY
(
y
);
};
templateHolder
.
current
.
addEventListener
(
"
mousedown
"
,
handleMouseDown
);
document
.
addEventListener
(
"
mousemove
"
,
handleMouseMove
);
document
.
addEventListener
(
"
mouseup
"
,
handleMouseUp
);
return
()
=>
{
instance
.
current
?.
destroy
();
templateHolderRef
?.
removeEventListener
(
"
mousedown
"
,
handleMouseDown
);
document
.
removeEventListener
(
"
mousemove
"
,
handleMouseMove
);
document
.
removeEventListener
(
"
mouseup
"
,
handleMouseUp
);
};
},
[]);
useEffect
(()
=>
{
if
(
!
instance
.
current
)
{
console
.
warn
(
"
[Template] Received template enable but no instance exists
"
);
return
;
}
instance
.
current
.
setOption
(
"
enable
"
,
enable
);
if
(
enable
&&
url
)
{
instance
.
current
.
loadImage
(
url
).
then
(()
=>
{
console
.
log
(
"
[Template] enable: load image finished
"
);
});
}
},
[
enable
]);
useEffect
(()
=>
{
if
(
!
instance
.
current
)
{
console
.
warn
(
"
[Template] Recieved template url update but no template instance exists
"
);
return
;
}
if
(
!
url
)
{
console
.
warn
(
"
[Template] Received template url blank
"
);
return
;
}
if
(
!
enable
)
{
console
.
info
(
"
[Template] Got template URL but not enabled, ignoring
"
);
return
;
}
instance
.
current
.
loadImage
(
url
).
then
(()
=>
{
console
.
log
(
"
[Template] Template loader finished
"
);
});
},
[
url
]);
useEffect
(()
=>
{
if
(
!
instance
.
current
)
{
console
.
warn
(
"
[Template] Received template width with no instance
"
);
return
;
}
instance
.
current
.
setOption
(
"
width
"
,
width
);
instance
.
current
.
rasterizeTemplate
();
},
[
width
]);
useEffect
(()
=>
{
if
(
!
instance
.
current
)
{
console
.
warn
(
"
[Template] Received style update with no instance
"
);
return
;
}
instance
.
current
.
setOption
(
"
style
"
,
style
);
},
[
style
]);
return
(
<
div
id
=
"template"
className
=
"board-overlay"
ref
=
{
templateHolder
}
style
=
{
{
top
:
y
,
left
:
x
,
opacity
:
opacity
/
100
,
}
}
></
div
>
);
};
packages/client/src/components/ToastWrapper.tsx
0 → 100644
View file @
51eacdaf
import
{
useTheme
}
from
"
next-themes
"
;
import
{
ToastContainer
}
from
"
react-toastify
"
;
export
const
ToastWrapper
=
()
=>
{
const
{
theme
}
=
useTheme
()
return
(
<
ToastContainer
position
=
"top-left"
theme
=
{
theme
}
/>
);
};
\ No newline at end of file
packages/client/src/components/CanvasMeta.tsx
→
packages/client/src/components/
Toolbar/
CanvasMeta.tsx
View file @
51eacdaf
...
...
@@ -6,11 +6,11 @@ import {
useDisclosure
,
}
from
"
@nextui-org/react
"
;
import
{
CanvasLib
}
from
"
@sc07-canvas/lib/src/canvas
"
;
import
{
useAppContext
}
from
"
../contexts/AppContext
"
;
import
{
Canvas
}
from
"
../lib/canvas
"
;
import
{
useAppContext
}
from
"
../
../
contexts/AppContext
"
;
import
{
Canvas
}
from
"
../
../
lib/canvas
"
;
import
{
useEffect
,
useState
}
from
"
react
"
;
import
{
ClientConfig
}
from
"
@sc07-canvas/lib/src/net
"
;
import
network
from
"
../lib/network
"
;
import
network
from
"
../
../
lib/network
"
;
const
getTimeLeft
=
(
pixels
:
{
available
:
number
},
config
:
ClientConfig
)
=>
{
// this implementation matches the server's implementation
...
...
@@ -18,6 +18,7 @@ const getTimeLeft = (pixels: { available: number }, config: ClientConfig) => {
const
cooldown
=
CanvasLib
.
getPixelCooldown
(
pixels
.
available
+
1
,
config
);
const
pixelExpiresAt
=
Canvas
.
instance
?.
lastPlace
&&
Canvas
.
instance
.
lastPlace
+
cooldown
*
1000
;
const
pixelCooldown
=
pixelExpiresAt
&&
(
Date
.
now
()
-
pixelExpiresAt
)
/
1000
;
if
(
!
pixelCooldown
)
return
undefined
;
...
...
@@ -27,7 +28,7 @@ const getTimeLeft = (pixels: { available: number }, config: ClientConfig) => {
};
const
PlaceCountdown
=
()
=>
{
const
{
pixels
,
config
}
=
useAppContext
();
const
{
pixels
,
config
}
=
useAppContext
<
true
>
();
const
[
timeLeft
,
setTimeLeft
]
=
useState
(
getTimeLeft
(
pixels
,
config
));
useEffect
(()
=>
{
...
...
@@ -43,7 +44,7 @@ const PlaceCountdown = () => {
return
(
<>
{
timeLeft
?
pixels
.
available
+
1
<
config
.
canvas
.
pixel
.
maxStack
&&
timeLeft
+
"
s
"
?
pixels
.
available
<
config
.
canvas
.
pixel
.
maxStack
&&
timeLeft
+
"
s
"
:
""
}
</>
);
...
...
@@ -57,7 +58,7 @@ const OnlineCount = () => {
setOnline
(
count
);
}
network
.
waitFor
(
"
online
"
).
then
(([
count
])
=>
setOnline
(
count
));
network
.
waitFor
State
(
"
online
"
).
then
(([
count
])
=>
setOnline
(
count
));
network
.
on
(
"
online
"
,
handleOnline
);
return
()
=>
{
...
...
@@ -69,22 +70,22 @@ const OnlineCount = () => {
};
export
const
CanvasMeta
=
()
=>
{
const
{
canvasPosition
,
cursor
Position
,
pixels
,
config
}
=
useAppContext
();
const
{
canvasPosition
,
cursor
,
pixels
,
config
}
=
useAppContext
<
true
>
();
const
{
isOpen
,
onOpen
,
onOpenChange
}
=
useDisclosure
();
return
(
<>
<
div
id
=
"canvas-meta"
>
<
div
id
=
"canvas-meta"
className
=
"toolbar-box"
>
{
canvasPosition
&&
(
<
span
>
<
button
className
=
"btn-link"
onClick
=
{
onOpen
}
>
(
{
canvasPosition
.
x
}
,
{
canvasPosition
.
y
}
)
</
button
>
{
cursor
Position
&&
(
{
cursor
.
x
!==
undefined
&&
cursor
.
y
!==
undefined
&&
(
<>
{
"
"
}
<
span
className
=
"canvas-meta--cursor-pos"
>
(Cursor:
{
cursor
Position
.
x
}
,
{
cursor
Position
.
y
}
)
(Cursor:
{
cursor
.
x
}
,
{
cursor
.
y
}
)
</
span
>
</>
)
}
...
...
packages/client/src/components/Pal
l
ete.scss
→
packages/client/src/components/
Toolbar/
Pale
t
te.scss
View file @
51eacdaf
#
pallete
{
#
toolbar
{
position
:
fixed
;
left
:
0
;
bottom
:
0
;
width
:
100%
;
}
#pallete
{
display
:
flex
;
gap
:
10px
;
padding
:
10px
;
background-color
:
#fff
;
z-index
:
10
;
position
:
relative
;
.pallete-colors
{
// display: flex;
...
...
@@ -38,7 +40,6 @@
vertical-align
:
top
;
font-size
:
2rem
;
line-height
:
1
;
color
:
#000
;
border
:
0
;
background
:
transparent
;
...
...
@@ -47,7 +48,7 @@
.pallete-color
{
width
:
36px
;
height
:
36px
;
border
:
2px
solid
#000
;
border
:
2px
solid
;
border-radius
:
3px
;
transition
:
transform
0
.25s
;
cursor
:
pointer
;
...
...
packages/client/src/components/Pal
l
ete.tsx
→
packages/client/src/components/
Toolbar/
Pale
t
te.tsx
View file @
51eacdaf
import
{
useEffect
,
useState
}
from
"
react
"
;
import
{
useAppContext
}
from
"
../contexts/AppContext
"
;
import
{
Canvas
}
from
"
../lib/canvas
"
;
import
{
IPalleteContext
}
from
"
../types
"
;
import
{
useEffect
}
from
"
react
"
;
import
{
useAppContext
}
from
"
../../contexts/AppContext
"
;
import
{
Canvas
}
from
"
../../lib/canvas
"
;
import
{
FontAwesomeIcon
}
from
"
@fortawesome/react-fontawesome
"
;
import
{
faXmark
}
from
"
@fortawesome/free-solid-svg-icons
"
;
import
{
CanvasMeta
}
from
"
./CanvasMeta
"
;
import
{
KeybindManager
}
from
"
../../lib/keybinds
"
;
import
{
Button
,
Link
}
from
"
@nextui-org/react
"
;
export
const
Pallete
=
()
=>
{
const
{
config
,
user
}
=
useAppContext
();
const
[
pallete
,
setPallete
]
=
useState
<
IPalleteContext
>
({});
export
const
Palette
=
()
=>
{
const
{
config
,
user
,
cursor
,
setCursor
}
=
useAppContext
<
true
>
();
useEffect
(()
=>
{
if
(
!
Canvas
.
instance
)
return
;
Canvas
.
instance
?.
updateCursor
(
cursor
.
color
);
},
[
cursor
]);
Canvas
.
instance
.
updatePallete
(
pallete
);
},
[
pallete
]);
useEffect
(()
=>
{
const
handleDeselect
=
()
=>
{
setCursor
((
v
)
=>
({
...
v
,
color
:
undefined
,
}));
};
return
(
<
div
id
=
"pallete"
>
<
CanvasMeta
/>
KeybindManager
.
addListener
(
"
DESELECT_COLOR
"
,
handleDeselect
);
return
()
=>
{
KeybindManager
.
removeListener
(
"
DESELECT_COLOR
"
,
handleDeselect
);
};
},
[]);
return
(
<
div
id
=
"pallete"
className
=
"bg-[#fff] dark:bg-[#000]"
>
<
div
className
=
"pallete-colors"
>
<
button
aria-label
=
"Deselect Color"
className
=
"pallete-color--deselect"
title
=
"Deselect Color"
onClick
=
{
()
=>
{
set
Pallete
(({
color
,
...
pallete
})
=>
{
return
pallete
;
set
Cursor
(({
color
,
...
cursor
})
=>
{
return
cursor
;
});
}
}
>
...
...
@@ -37,7 +47,7 @@ export const Pallete = () => {
<
button
key
=
{
color
.
id
}
aria-label
=
{
color
.
name
}
className
=
{
[
"
pallete-color
"
,
color
.
id
===
pallete
.
color
&&
"
active
"
]
className
=
{
[
"
pallete-color
"
,
color
.
id
===
cursor
.
color
&&
"
active
"
]
.
filter
((
a
)
=>
a
)
.
join
(
"
"
)
}
style
=
{
{
...
...
@@ -45,9 +55,9 @@ export const Pallete = () => {
}
}
title
=
{
color
.
name
}
onClick
=
{
()
=>
{
set
Pallete
((
pallete
)
=>
{
set
Cursor
((
cursor
)
=>
{
return
{
...
pallete
,
...
cursor
,
color
:
color
.
id
,
};
});
...
...
@@ -58,10 +68,21 @@ export const Pallete = () => {
{
!
user
&&
(
<
div
className
=
"pallete-user-overlay"
>
You are not logged in
<
a
href
=
"/api/login"
className
=
"user-login"
>
Login
</
a
>
{
import
.
meta
.
env
.
VITE_INCLUDE_EVENT_INFO
?
(
<>
The event has ended
</>
)
:
(
<
div
className
=
"flex gap-3 items-center"
>
You are not logged in
<
Button
as
=
{
Link
}
href
=
"/api/login"
className
=
"user-login"
variant
=
"faded"
>
Login
</
Button
>
</
div
>
)
}
</
div
>
)
}
</
div
>
...
...
packages/client/src/components/Toolbar/ToolbarWrapper.tsx
0 → 100644
View file @
51eacdaf
import
{
useAppContext
}
from
"
../../contexts/AppContext
"
;
import
{
useTemplateContext
}
from
"
../../contexts/TemplateContext
"
;
import
{
MobileTemplateButtons
}
from
"
../Templating/MobileTemplateButtons
"
;
import
{
CanvasMeta
}
from
"
./CanvasMeta
"
;
import
{
Palette
}
from
"
./Palette
"
;
import
{
UndoButton
}
from
"
./UndoButton
"
;
/**
* Wrapper for everything aligned at the bottom of the screen
*/
export
const
ToolbarWrapper
=
()
=>
{
const
{
config
}
=
useAppContext
();
const
{
showMobileTools
}
=
useTemplateContext
();
if
(
!
config
)
return
<></>;
return
(
<
div
id
=
"toolbar"
>
<
CanvasMeta
/>
<
UndoButton
/>
{
showMobileTools
&&
<
MobileTemplateButtons
/>
}
<
Palette
/>
</
div
>
);
};
packages/client/src/components/Toolbar/UndoButton.tsx
0 → 100644
View file @
51eacdaf
import
{
Button
}
from
"
@nextui-org/react
"
;
import
{
useAppContext
}
from
"
../../contexts/AppContext
"
;
import
network
from
"
../../lib/network
"
;
import
{
useEffect
,
useState
}
from
"
react
"
;
import
{
toast
}
from
"
react-toastify
"
;
export
const
UndoButton
=
()
=>
{
const
{
undo
,
config
}
=
useAppContext
<
true
>
();
/**
* percentage of time left (0 <= x <= 1)
*/
const
[
progress
,
setProgress
]
=
useState
(
0.5
);
useEffect
(()
=>
{
if
(
!
undo
)
{
setProgress
(
1
);
return
;
}
const
timer
=
setInterval
(()
=>
{
let
diff
=
undo
.
expireAt
-
Date
.
now
();
let
percentage
=
diff
/
config
.
canvas
.
undo
.
grace_period
;
setProgress
(
percentage
);
if
(
percentage
<=
0
)
{
clearInterval
(
timer
);
}
},
100
);
return
()
=>
{
clearInterval
(
timer
);
};
},
[
undo
]);
// ref-ify this?
function
execUndo
()
{
network
.
socket
.
emitWithAck
(
"
undo
"
).
then
((
data
)
=>
{
if
(
data
.
success
)
{
console
.
log
(
"
Undo pixel successful
"
);
}
else
{
console
.
log
(
"
Undo pixel error
"
,
data
);
switch
(
data
.
error
)
{
case
"
pixel_covered
"
:
toast
.
error
(
"
You cannot undo a covered pixel
"
);
break
;
case
"
unavailable
"
:
toast
.
error
(
"
You have no undo available
"
);
break
;
default
:
toast
.
error
(
"
Undo error:
"
+
data
.
error
);
}
}
});
}
return
(
<
div
className
=
"absolute z-0"
style
=
{
{
top
:
undo
?.
available
&&
progress
>=
0
?
"
-10px
"
:
"
100%
"
,
left
:
"
50%
"
,
transform
:
"
translateY(-100%) translateX(-50%)
"
,
transition
:
"
all 0.25s ease-in-out
"
,
}
}
>
<
Button
onPress
=
{
execUndo
}
>
<
span
className
=
"z-[1]"
>
Undo
</
span
>
<
div
className
=
"absolute top-0 left-0 h-full bg-white/50 transition-all"
style
=
{
{
width
:
progress
*
100
+
"
%
"
}
}
></
div
>
</
Button
>
</
div
>
);
};
packages/client/src/components/Welcome/WelcomeModal.tsx
0 → 100644
View file @
51eacdaf
import
{
Button
,
Modal
,
ModalBody
,
ModalContent
,
ModalFooter
,
ModalHeader
,
useDisclosure
,
}
from
"
@nextui-org/react
"
;
/**
* Welcome popup
*
* TODO: customization post-event (#46)
*
* @returns
*/
export
const
WelcomeModal
=
()
=>
{
const
{
isOpen
,
onClose
}
=
useDisclosure
({
defaultOpen
:
!
localStorage
.
getItem
(
"
hide_welcome
"
),
});
const
handleClose
=
()
=>
{
localStorage
.
setItem
(
"
hide_welcome
"
,
"
true
"
);
onClose
();
};
return
(
<
Modal
isOpen
=
{
isOpen
}
onClose
=
{
handleClose
}
isDismissable
=
{
false
}
placement
=
"center"
>
<
ModalContent
>
{
(
onClose
)
=>
(
<>
<
ModalHeader
>
Welcome
</
ModalHeader
>
<
ModalBody
>
<
h1
className
=
"text-4xl text-center"
>
Welcome to Canvas!
</
h1
>
<
p
>
Canvas is a collaborative pixel placing event that uses
Fediverse accounts
</
p
>
<
p
>
More information can be found in the top left
</
p
>
</
ModalBody
>
<
ModalFooter
>
<
Button
onPress
=
{
onClose
}
>
Close
</
Button
>
</
ModalFooter
>
</>
)
}
</
ModalContent
>
</
Modal
>
);
};
packages/client/src/contexts/AppContext.tsx
View file @
51eacdaf
import
{
import
React
,
{
PropsWithChildren
,
createContext
,
useContext
,
useEffect
,
useState
,
}
from
"
react
"
;
import
{
AuthSession
,
ClientConfig
,
IAppContext
,
ICanvasPosition
,
IPosition
,
}
from
"
@sc07-canvas/lib/src/net
"
;
import
{
AuthSession
,
ClientConfig
}
from
"
@sc07-canvas/lib/src/net
"
;
import
Network
from
"
../lib/network
"
;
import
{
Spinner
}
from
"
@nextui-org/react
"
;
import
{
api
}
from
"
../lib/utils
"
;
interface
IAppContext
{
config
?:
ClientConfig
;
user
?:
AuthSession
;
connected
:
boolean
;
canvasPosition
?:
ICanvasPosition
;
setCanvasPosition
:
(
v
:
ICanvasPosition
)
=>
void
;
cursor
:
ICursor
;
setCursor
:
React
.
Dispatch
<
React
.
SetStateAction
<
ICursor
>>
;
pixels
:
{
available
:
number
};
undo
?:
{
available
:
true
;
expireAt
:
number
};
loadChat
:
boolean
;
setLoadChat
:
(
v
:
boolean
)
=>
void
;
infoSidebar
:
boolean
;
setInfoSidebar
:
(
v
:
boolean
)
=>
void
;
settingsSidebar
:
boolean
;
setSettingsSidebar
:
(
v
:
boolean
)
=>
void
;
pixelWhois
?:
{
x
:
number
;
y
:
number
;
surrounding
:
string
[][]
};
setPixelWhois
:
(
v
:
this
[
"
pixelWhois
"
])
=>
void
;
showKeybinds
:
boolean
;
setShowKeybinds
:
(
v
:
boolean
)
=>
void
;
blankOverlay
:
IMapOverlay
;
setBlankOverlay
:
React
.
Dispatch
<
React
.
SetStateAction
<
IMapOverlay
>>
;
heatmapOverlay
:
IMapOverlay
;
setHeatmapOverlay
:
React
.
Dispatch
<
React
.
SetStateAction
<
IMapOverlay
>>
;
pixelPulses
:
boolean
;
setPixelPulses
:
(
state
:
boolean
)
=>
void
;
profile
?:
string
;
// sub
setProfile
:
(
v
?:
string
)
=>
void
;
hasAdmin
:
boolean
;
showModModal
:
boolean
;
setShowModModal
:
React
.
Dispatch
<
React
.
SetStateAction
<
boolean
>>
;
}
interface
ICanvasPosition
{
x
:
number
;
y
:
number
;
zoom
:
number
;
}
interface
ICursor
{
x
?:
number
;
y
?:
number
;
color
?:
number
;
}
interface
IMapOverlay
{
enabled
:
boolean
;
/**
* opacity of the overlay
* 0.0 - 1.0
*/
opacity
:
number
;
loading
:
boolean
;
}
const
appContext
=
createContext
<
IAppContext
>
({}
as
any
);
export
const
useAppContext
=
()
=>
useContext
(
appContext
);
type
WithRequiredProperty
<
Type
,
Key
extends
keyof
Type
>
=
Type
&
{
[
Property
in
Key
]
-
?:
Type
[
Property
];
};
type
AppContext
<
ConfigExists
extends
boolean
>
=
ConfigExists
extends
true
?
WithRequiredProperty
<
IAppContext
,
"
config
"
>
:
IAppContext
;
/**
* Get app context
*
* @template ConfigExists If the config is already known to be available in this context
* @returns
*/
export
const
useAppContext
=
<
ConfigExists
extends
boolean
=
false
>
() =>
useContext
<
AppContext
<
ConfigExists
>>
(
appContext
as
any
);
// eslint-disable-next-line @typescript-eslint/no-redeclare
export
const
AppContext
=
({
children
}:
PropsWithChildren
)
=>
{
const
[
config
,
setConfig
]
=
useState
<
ClientConfig
>
(
undefined
as
any
);
const
[
auth
,
setAuth
]
=
useState
<
AuthSession
>
();
const
[
canvasPosition
,
setCanvasPosition
]
=
useState
<
ICanvasPosition
>
();
const
[
cursorPosition
,
setCursorPosition
]
=
useState
<
IPosition
>
();
const
[
cursor
,
setCursor
]
=
useState
<
ICursor
>
({});
const
[
connected
,
setConnected
]
=
useState
(
false
);
// --- settings ---
const
[
loadChat
,
_setLoadChat
]
=
useState
(
false
);
const
[
pixels
,
setPixels
]
=
useState
({
available
:
0
});
const
[
undo
,
setUndo
]
=
useState
<
{
available
:
true
;
expireAt
:
number
}
>
();
// overlays visible
const
[
infoSidebar
,
setInfoSidebar
]
=
useState
(
false
);
const
[
settingsSidebar
,
setSettingsSidebar
]
=
useState
(
false
);
const
[
pixelWhois
,
setPixelWhois
]
=
useState
<
{
x
:
number
;
y
:
number
;
surrounding
:
string
[][];
}
>
();
const
[
showKeybinds
,
setShowKeybinds
]
=
useState
(
false
);
const
[
blankOverlay
,
setBlankOverlay
]
=
useState
<
IMapOverlay
>
({
enabled
:
false
,
opacity
:
1
,
loading
:
false
,
});
const
[
heatmapOverlay
,
setHeatmapOverlay
]
=
useState
<
IMapOverlay
>
({
enabled
:
false
,
opacity
:
1
,
loading
:
false
,
});
const
[
pixelPulses
,
setPixelPulses
]
=
useState
<
boolean
>
(
false
);
const
[
profile
,
setProfile
]
=
useState
<
string
>
();
const
[
hasAdmin
,
setHasAdmin
]
=
useState
(
false
);
const
[
showModModal
,
setShowModModal
]
=
useState
(
false
);
useEffect
(()
=>
{
function
loadSettings
()
{
setLoadChat
(
localStorage
.
getItem
(
"
matrix.enable
"
)
===
null
?
true
:
localStorage
.
getItem
(
"
matrix.enable
"
)
===
"
true
"
);
}
function
handleConfig
(
config
:
ClientConfig
)
{
console
.
info
(
"
Server sent config
"
,
config
);
setConfig
(
config
);
}
...
...
@@ -40,20 +154,60 @@ export const AppContext = ({ children }: PropsWithChildren) => {
setPixels
(
pixels
);
}
function
handleUndo
(
data
:
{
available
:
false
}
|
{
available
:
true
;
expireAt
:
number
}
)
{
if
(
data
.
available
)
{
setUndo
({
available
:
true
,
expireAt
:
data
.
expireAt
});
}
else
{
setUndo
(
undefined
);
}
}
function
handleConnect
()
{
setConnected
(
true
);
}
function
handleDisconnect
()
{
setConnected
(
false
);
}
api
<
{}
>
(
"
/api/admin/check
"
).
then
(({
status
,
data
})
=>
{
if
(
status
===
200
)
{
if
(
data
.
success
)
{
setHasAdmin
(
true
);
}
}
});
Network
.
on
(
"
user
"
,
handleUser
);
Network
.
on
(
"
config
"
,
handleConfig
);
Network
.
waitFor
(
"
pixels
"
).
then
(([
data
])
=>
handlePixels
(
data
));
Network
.
waitFor
State
(
"
pixels
"
).
then
(([
data
])
=>
handlePixels
(
data
));
Network
.
on
(
"
pixels
"
,
handlePixels
);
Network
.
on
(
"
undo
"
,
handleUndo
);
Network
.
on
(
"
connected
"
,
handleConnect
);
Network
.
on
(
"
disconnected
"
,
handleDisconnect
);
Network
.
socket
.
connect
();
loadSettings
();
return
()
=>
{
Network
.
off
(
"
user
"
,
handleUser
);
Network
.
off
(
"
config
"
,
handleConfig
);
Network
.
off
(
"
pixels
"
,
handlePixels
);
Network
.
off
(
"
undo
"
,
handleUndo
);
Network
.
off
(
"
connected
"
,
handleConnect
);
Network
.
off
(
"
disconnected
"
,
handleDisconnect
);
};
},
[]);
const
setLoadChat
=
(
v
:
boolean
)
=>
{
_setLoadChat
(
v
);
localStorage
.
setItem
(
"
matrix.enable
"
,
v
?
"
true
"
:
"
false
"
);
};
return
(
<
appContext
.
Provider
value
=
{
{
...
...
@@ -61,12 +215,40 @@ export const AppContext = ({ children }: PropsWithChildren) => {
user
:
auth
,
canvasPosition
,
setCanvasPosition
,
cursor
Position
,
setCursor
Position
,
cursor
,
setCursor
,
pixels
,
settingsSidebar
,
setSettingsSidebar
,
undo
,
loadChat
,
setLoadChat
,
connected
,
hasAdmin
,
pixelWhois
,
setPixelWhois
,
showKeybinds
,
setShowKeybinds
,
blankOverlay
,
setBlankOverlay
,
heatmapOverlay
,
setHeatmapOverlay
,
pixelPulses
,
setPixelPulses
,
profile
,
setProfile
,
infoSidebar
,
setInfoSidebar
,
showModModal
,
setShowModModal
,
}
}
>
{
config
?
children
:
"
Loading...
"
}
{
!
config
&&
(
<
div
className
=
"fixed top-0 left-0 w-full h-full z-[49] backdrop-blur-sm bg-black/30 text-white flex items-center justify-center"
>
<
Spinner
label
=
"Loading..."
/>
</
div
>
)
}
{
children
}
</
appContext
.
Provider
>
);
};
packages/client/src/contexts/ChatContext.tsx
0 → 100644
View file @
51eacdaf
import
{
PropsWithChildren
,
createContext
,
useContext
,
useEffect
,
useRef
,
useState
,
}
from
"
react
"
;
import
{
useAppContext
}
from
"
./AppContext
"
;
import
{
toast
}
from
"
react-toastify
"
;
interface
IMatrixUser
{
userId
:
string
;
}
export
interface
IChatContext
{
user
?:
IMatrixUser
;
notificationCount
:
number
;
doLogin
:
()
=>
void
;
doLogout
:
()
=>
Promise
<
void
>
;
}
const
chatContext
=
createContext
<
IChatContext
>
({}
as
any
);
export
const
useChatContext
=
()
=>
useContext
(
chatContext
);
export
const
ChatContext
=
({
children
}:
PropsWithChildren
)
=>
{
const
{
config
}
=
useAppContext
();
const
checkInterval
=
useRef
<
ReturnType
<
typeof
setInterval
>>
();
const
checkNotifs
=
useRef
<
ReturnType
<
typeof
setInterval
>>
();
const
[
user
,
setUser
]
=
useState
<
IMatrixUser
>
();
const
[
notifs
,
setNotifs
]
=
useState
(
0
);
const
doLogin
=
()
=>
{
if
(
!
config
)
{
console
.
warn
(
"
[ChatContext#doLogin] has no config instance
"
);
return
;
}
if
(
user
?.
userId
)
{
console
.
log
(
"
[ChatContext#doLogin] user logged in, opening element...
"
);
window
.
open
(
config
.
chat
.
element_host
);
return
;
}
const
redirectUrl
=
window
.
location
.
protocol
+
"
//
"
+
window
.
location
.
host
+
"
/chat_callback
"
;
window
.
addEventListener
(
"
focus
"
,
handleWindowFocus
);
checkInterval
.
current
=
setInterval
(
checkForAccessToken
,
500
);
window
.
open
(
`https://
${
config
.
chat
.
matrix_homeserver
}
/_matrix/client/v3/login/sso/redirect?redirectUrl=
${
encodeURIComponent
(
redirectUrl
)}
`
,
"
_blank
"
);
};
const
doLogout
=
async
()
=>
{
if
(
!
config
)
{
console
.
warn
(
"
[ChatContext#doLogout] has no config instance
"
);
return
;
}
await
fetch
(
`https://
${
config
.
chat
.
matrix_homeserver
}
/_matrix/client/v3/logout`
,
{
method
:
"
POST
"
,
headers
:
{
Authorization
:
"
Bearer
"
+
localStorage
.
getItem
(
"
matrix.access_token
"
),
},
}
);
localStorage
.
removeItem
(
"
matrix.access_token
"
);
localStorage
.
removeItem
(
"
matrix.device_id
"
);
localStorage
.
removeItem
(
"
matrix.user_id
"
);
setUser
(
undefined
);
};
useEffect
(()
=>
{
checkForAccessToken
();
checkNotifs
.
current
=
setInterval
(
checkForNotifs
,
1000
*
60
);
return
()
=>
{
window
.
removeEventListener
(
"
focus
"
,
handleWindowFocus
);
if
(
checkInterval
.
current
)
clearInterval
(
checkInterval
.
current
);
if
(
checkNotifs
.
current
)
clearInterval
(
checkNotifs
.
current
);
};
},
[
config
?.
chat
]);
const
handleWindowFocus
=
()
=>
{
console
.
log
(
"
[Chat] Window has gained focus
"
);
checkForAccessToken
();
};
const
checkForAccessToken
=
()
=>
{
const
accessToken
=
localStorage
.
getItem
(
"
matrix.access_token
"
);
const
deviceId
=
localStorage
.
getItem
(
"
matrix.device_id
"
);
const
userId
=
localStorage
.
getItem
(
"
matrix.user_id
"
);
if
(
!
accessToken
||
!
deviceId
||
!
userId
)
return
;
// access token acquired
window
.
removeEventListener
(
"
focus
"
,
handleWindowFocus
);
if
(
checkInterval
.
current
)
clearInterval
(
checkInterval
.
current
);
console
.
log
(
"
[Chat] access token has been acquired
"
);
setUser
({
userId
});
toast
.
success
(
"
Logged into chat
"
);
checkIfInGeneral
();
};
const
checkIfInGeneral
=
async
()
=>
{
const
generalAlias
=
config
?.
chat
.
general_alias
;
if
(
!
generalAlias
)
{
console
.
log
(
"
[ChatContext#checkIfInGeneral] no general alias in config
"
);
return
;
}
const
accessToken
=
localStorage
.
getItem
(
"
matrix.access_token
"
);
if
(
!
accessToken
)
return
;
const
joinReq
=
await
fetch
(
`https://
${
config
.
chat
.
matrix_homeserver
}
/_matrix/client/v3/join/
${
generalAlias
}
`
,
{
method
:
"
POST
"
,
headers
:
{
Authorization
:
"
Bearer
"
+
accessToken
,
"
Content-Type
"
:
"
application/json
"
,
},
body
:
JSON
.
stringify
({
reason
:
"
Auto-joined via Canvas client
"
,
}),
}
);
const
joinRes
=
await
joinReq
.
json
();
console
.
log
(
"
[ChatContext#checkIfInGeneral] auto-join general response
"
,
joinRes
);
if
(
joinReq
.
status
===
200
)
{
toast
.
success
(
`Joined chat
${
decodeURIComponent
(
generalAlias
)}
!`
);
}
else
if
(
joinReq
.
status
===
403
)
{
toast
.
error
(
"
Failed to join general chat!
"
+
joinRes
.
errcode
+
"
-
"
+
joinRes
.
error
);
}
else
if
(
joinReq
.
status
===
429
)
{
toast
.
warn
(
"
Auto-join general chat got ratelimited
"
);
}
else
{
toast
.
error
(
"
Failed to join general chat!
"
+
joinRes
.
errcode
+
"
-
"
+
joinRes
.
error
);
}
};
const
checkForNotifs
=
async
()
=>
{
if
(
!
config
)
{
console
.
warn
(
"
[ChatContext#checkForNotifs] no config instance
"
);
return
;
}
const
accessToken
=
localStorage
.
getItem
(
"
matrix.access_token
"
);
if
(
!
accessToken
)
return
;
const
notifReq
=
await
fetch
(
`https://
${
config
.
chat
.
matrix_homeserver
}
/_matrix/client/v3/notifications?limit=10`
,
{
headers
:
{
Authorization
:
"
Bearer
"
+
accessToken
,
},
}
);
const
notifRes
=
await
notifReq
.
json
();
const
notificationCount
=
notifRes
?.
notifications
?.
filter
((
n
:
any
)
=>
!
n
.
read
).
length
||
0
;
setNotifs
(
notificationCount
);
};
return
(
<
chatContext
.
Provider
value
=
{
{
user
,
notificationCount
:
notifs
,
doLogin
,
doLogout
}
}
>
{
children
}
</
chatContext
.
Provider
>
);
};
packages/client/src/contexts/TemplateContext.tsx
0 → 100644
View file @
51eacdaf
import
{
PropsWithChildren
,
createContext
,
useContext
,
useEffect
,
useRef
,
useState
,
}
from
"
react
"
;
import
{
IRouterData
,
Router
}
from
"
../lib/router
"
;
import
{
KeybindManager
}
from
"
../lib/keybinds
"
;
import
{
TemplateStyle
}
from
"
../lib/template
"
;
interface
ITemplate
{
/**
* If the template is being used
*/
enable
:
boolean
;
/**
* URL of the template image being used
*/
url
?:
string
;
/**
* Width of the template being displayed
*
* @default min(template.width,canvas.width)
*/
width
?:
number
;
x
:
number
;
y
:
number
;
opacity
:
number
;
style
:
TemplateStyle
;
showMobileTools
:
boolean
;
setEnable
(
v
:
boolean
):
void
;
setURL
(
v
?:
string
):
void
;
setWidth
(
v
?:
number
):
void
;
setX
(
v
:
number
):
void
;
setY
(
v
:
number
):
void
;
setOpacity
(
v
:
number
):
void
;
setStyle
(
style
:
TemplateStyle
):
void
;
setShowMobileTools
(
v
:
boolean
):
void
;
}
const
templateContext
=
createContext
<
ITemplate
>
({}
as
any
);
export
const
useTemplateContext
=
()
=>
useContext
(
templateContext
);
export
const
TemplateContext
=
({
children
}:
PropsWithChildren
)
=>
{
const
routerData
=
Router
.
get
();
const
[
enable
,
setEnable
]
=
useState
(
!!
routerData
.
template
?.
url
);
const
[
url
,
setURL
]
=
useState
<
string
|
undefined
>
(
routerData
.
template
?.
url
);
const
[
width
,
setWidth
]
=
useState
<
number
|
undefined
>
(
routerData
.
template
?.
width
);
const
[
x
,
setX
]
=
useState
(
routerData
.
template
?.
x
||
0
);
const
[
y
,
setY
]
=
useState
(
routerData
.
template
?.
y
||
0
);
const
[
opacity
,
setOpacity
]
=
useState
(
100
);
const
[
style
,
setStyle
]
=
useState
<
TemplateStyle
>
(
routerData
.
template
?.
style
||
"
ONE_TO_ONE
"
);
const
[
showMobileTools
,
setShowMobileTools
]
=
useState
(
true
);
const
initAt
=
useRef
<
number
>
();
useEffect
(()
=>
{
initAt
.
current
=
Date
.
now
();
const
handleNavigate
=
(
data
:
IRouterData
)
=>
{
if
(
data
.
template
)
{
setEnable
(
true
);
setURL
(
data
.
template
.
url
);
setWidth
(
data
.
template
.
width
);
setX
(
data
.
template
.
x
||
0
);
setY
(
data
.
template
.
y
||
0
);
setStyle
(
data
.
template
.
style
||
"
ONE_TO_ONE
"
);
}
else
{
setEnable
(
false
);
}
};
const
handleToggleTemplate
=
()
=>
{
setEnable
((
en
)
=>
!
en
);
};
Router
.
on
(
"
navigate
"
,
handleNavigate
);
KeybindManager
.
on
(
"
TOGGLE_TEMPLATE
"
,
handleToggleTemplate
);
return
()
=>
{
Router
.
off
(
"
navigate
"
,
handleNavigate
);
KeybindManager
.
on
(
"
TOGGLE_TEMPLATE
"
,
handleToggleTemplate
);
};
},
[]);
useEffect
(()
=>
{
Router
.
setTemplate
({
enabled
:
enable
,
width
,
x
,
y
,
url
,
style
});
if
(
!
initAt
.
current
)
{
console
.
debug
(
"
TemplateContext updating router but no initAt
"
);
}
else
if
(
Date
.
now
()
-
initAt
.
current
<
2
*
1000
)
{
console
.
debug
(
"
TemplateContext updating router too soon after init
"
,
Date
.
now
()
-
initAt
.
current
);
}
if
(
initAt
.
current
&&
Date
.
now
()
-
initAt
.
current
>
2
*
1000
)
Router
.
queueUpdate
();
},
[
enable
,
width
,
x
,
y
,
url
,
style
]);
return
(
<
templateContext
.
Provider
value
=
{
{
enable
,
setEnable
,
url
,
setURL
,
width
,
setWidth
,
x
,
setX
,
y
,
setY
,
opacity
,
setOpacity
,
style
,
setStyle
,
showMobileTools
,
setShowMobileTools
,
}
}
>
{
children
}
</
templateContext
.
Provider
>
);
};
packages/client/src/index.html
View file @
51eacdaf
...
...
@@ -4,12 +4,15 @@
<meta
charset=
"UTF-8"
/>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
/>
<meta
http-equiv=
"X-UA-Compatible"
content=
"ie=edge"
/>
<title>
c
anvas
</title>
<title>
C
anvas
</title>
<link
rel=
"stylesheet"
href=
"./style.scss"
/>
</head>
<body>
<!-- Mastodon verification -->
<a
href=
"https://social.fediverse.events/@canvas"
rel=
"me"
></a>
<div
id=
"root"
></div>
<script
type=
"module"
src=
"./index.tsx"
></script>
...
...
packages/client/src/index.tsx
View file @
51eacdaf
import
"
./lib/sentry
"
;
import
React
from
"
react
"
;
import
{
createRoot
}
from
"
react-dom/client
"
;
import
{
NextUIProvider
}
from
"
@nextui-org/react
"
;
import
{
ThemeProvider
}
from
"
next-themes
"
;
import
App
from
"
./components/App
"
;
import
*
as
Sentry
from
"
@sentry/react
"
;
let
ErrorBoundary
=
({
children
}:
React
.
PropsWithChildren
)
=>
<>
{
children
}
</>;
if
(
__SENTRY_DSN__
)
{
ErrorBoundary
=
({
children
})
=>
{
return
<
Sentry
.
ErrorBoundary
showDialog
>
{
children
}
</
Sentry
.
ErrorBoundary
>;
};
}
const
root
=
createRoot
(
document
.
getElementById
(
"
root
"
)
!
);
root
.
render
(
<
React
.
StrictMode
>
<
NextUIProvider
>
<
App
/>
</
NextUIProvider
>
<
ErrorBoundary
>
<
NextUIProvider
>
<
ThemeProvider
attribute
=
"class"
defaultTheme
=
"system"
>
<
div
className
=
"w-screen h-screen bg-[#ddd] dark:bg-[#060606]"
>
<
App
/>
</
div
>
</
ThemeProvider
>
</
NextUIProvider
>
</
ErrorBoundary
>
</
React
.
StrictMode
>
);
Prev
1
2
3
4
5
6
7
8
9
Next