Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
S
SpeakEzy_Frontend
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Thisara Kavinda
SpeakEzy_Frontend
Commits
e6ce68d9
Commit
e6ce68d9
authored
Mar 15, 2024
by
Thisara Kavinda
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: implemented the pinned grid
parent
16cb267f
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
354 additions
and
47 deletions
+354
-47
src/Components/MeetingLayout/MeetingLayout.tsx
src/Components/MeetingLayout/MeetingLayout.tsx
+35
-8
src/Components/PinnedGrid/PinnedGrid.tsx
src/Components/PinnedGrid/PinnedGrid.tsx
+168
-0
src/Components/SymmetricGrid/SymmetricGrid.tsx
src/Components/SymmetricGrid/SymmetricGrid.tsx
+22
-37
src/Components/UserCard/UserCard.tsx
src/Components/UserCard/UserCard.tsx
+127
-0
src/Services/AgoraManager/AgoraManager.tsx
src/Services/AgoraManager/AgoraManager.tsx
+2
-2
No files found.
src/Components/MeetingLayout/MeetingLayout.tsx
View file @
e6ce68d9
...
@@ -6,21 +6,28 @@ import {
...
@@ -6,21 +6,28 @@ import {
useLocalMicrophoneTrack
,
useLocalMicrophoneTrack
,
useRemoteUsers
,
useRemoteUsers
,
usePublish
,
usePublish
,
useRTCClient
,
useClientEvent
,
}
from
'
agora-rtc-react
'
}
from
'
agora-rtc-react
'
import
SymmetricGrid
from
'
../SymmetricGrid/SymmetricGrid
'
import
SymmetricGrid
from
'
../SymmetricGrid/SymmetricGrid
'
import
PinnedGrid
from
'
../PinnedGrid/PinnedGrid
'
import
type
{
IAgoraRTCRemoteUser
}
from
'
agora-rtc-sdk-ng
'
const
MeetingLayout
=
()
=>
{
const
MeetingLayout
=
()
=>
{
const
theme
:
Theme
=
useTheme
()
const
theme
:
Theme
=
useTheme
()
const
agoraEngine
=
useRTCClient
()
const
{
localCameraTrack
}
=
useLocalCameraTrack
()
const
{
localCameraTrack
}
=
useLocalCameraTrack
()
const
{
localMicrophoneTrack
}
=
useLocalMicrophoneTrack
()
const
{
localMicrophoneTrack
}
=
useLocalMicrophoneTrack
()
usePublish
([
localMicrophoneTrack
,
localCameraTrack
])
usePublish
([
localMicrophoneTrack
,
localCameraTrack
])
const
remoteUsers
=
useRemoteUsers
()
const
remoteUsers
:
IAgoraRTCRemoteUser
[]
=
useRemoteUsers
()
const
gridContainerRef
=
useRef
<
HTMLDivElement
>
(
null
)
const
gridContainerRef
=
useRef
<
HTMLDivElement
>
(
null
)
const
[
gridContainerHeight
,
setGridContainerHeight
]
=
useState
(
0
)
const
[
gridContainerHeight
,
setGridContainerHeight
]
=
useState
(
0
)
const
[
totalUsers
,
setTotalUsers
]
=
useState
(
20
)
const
[
totalUsers
,
setTotalUsers
]
=
useState
(
0
)
const
[
isPinned
,
setIsPinned
]
=
useState
(
false
)
const
[
pinnedUser
,
setPinnedUser
]
=
useState
<
IAgoraRTCRemoteUser
|
null
>
(
null
)
useEffect
(()
=>
{
useEffect
(()
=>
{
setTotalUsers
(
remoteUsers
.
length
+
1
)
setTotalUsers
(
remoteUsers
.
length
+
1
)
...
@@ -32,6 +39,15 @@ const MeetingLayout = () => {
...
@@ -32,6 +39,15 @@ const MeetingLayout = () => {
}
}
},
[
gridContainerRef
?.
current
?.
offsetHeight
])
},
[
gridContainerRef
?.
current
?.
offsetHeight
])
const
handleUserLeft
=
(
user
:
IAgoraRTCRemoteUser
)
=>
{
if
(
user
.
uid
===
pinnedUser
?.
uid
)
{
setIsPinned
(
false
)
setPinnedUser
(
null
)
}
}
useClientEvent
(
agoraEngine
,
'
user-left
'
,
handleUserLeft
)
return
(
return
(
<
Box
<
Box
sx=
{
{
sx=
{
{
...
@@ -52,12 +68,23 @@ const MeetingLayout = () => {
...
@@ -52,12 +68,23 @@ const MeetingLayout = () => {
}
}
}
}
ref=
{
gridContainerRef
}
ref=
{
gridContainerRef
}
>
>
<
SymmetricGrid
{
isPinned
&&
pinnedUser
?
(
totalUsers=
{
totalUsers
}
<
PinnedGrid
setTotalUsers=
{
setTotalUsers
}
totalUsers=
{
totalUsers
}
gridContainerHeight=
{
gridContainerHeight
}
gridContainerHeight=
{
gridContainerHeight
}
setGridContainerHeight=
{
setGridContainerHeight
}
setIsPinned=
{
setIsPinned
}
/>
pinnedUser=
{
pinnedUser
}
setPinnedUser=
{
setPinnedUser
}
/>
)
:
(
<
SymmetricGrid
totalUsers=
{
totalUsers
}
gridContainerHeight=
{
gridContainerHeight
}
isPinned=
{
isPinned
}
setIsPinned=
{
setIsPinned
}
setPinnedUser=
{
setPinnedUser
}
/>
)
}
</
Box
>
</
Box
>
</
Box
>
</
Box
>
)
)
...
...
src/Components/PinnedGrid/PinnedGrid.tsx
0 → 100644
View file @
e6ce68d9
import
React
,
{
useEffect
}
from
'
react
'
import
type
{
IAgoraRTCRemoteUser
}
from
'
agora-rtc-sdk-ng
'
import
{
Box
,
Button
,
IconButton
,
Popover
,
Stack
}
from
'
@mui/material
'
import
useMediaQuery
from
'
@mui/material/useMediaQuery
'
import
{
RemoteUser
,
useRemoteUsers
}
from
'
agora-rtc-react
'
import
{
useTheme
,
type
Theme
}
from
'
@mui/material/styles
'
import
UserCard
from
'
../UserCard/UserCard
'
import
MoreVertIcon
from
'
@mui/icons-material/MoreVert
'
interface
Props
{
totalUsers
:
number
gridContainerHeight
:
number
setIsPinned
:
(
value
:
boolean
)
=>
void
pinnedUser
:
IAgoraRTCRemoteUser
setPinnedUser
:
(
value
:
IAgoraRTCRemoteUser
|
null
)
=>
void
}
const
PinnedGrid
=
({
totalUsers
,
gridContainerHeight
,
setIsPinned
,
pinnedUser
,
setPinnedUser
,
}:
Props
)
=>
{
const
theme
:
Theme
=
useTheme
()
const
isSm
=
useMediaQuery
(
theme
.
breakpoints
.
up
(
'
sm
'
))
const
remoteUsers
=
useRemoteUsers
()
const
[
anchorEl
,
setAnchorEl
]
=
React
.
useState
<
HTMLButtonElement
|
null
>
(
null
)
const
[
itemHeight
,
setItemHeight
]
=
React
.
useState
((
gridContainerHeight
-
16
*
3
)
/
4
)
const
open
=
Boolean
(
anchorEl
)
const
menuId
=
open
?
'
simple-popover
'
:
undefined
const
handleClickMenu
=
(
event
:
React
.
MouseEvent
<
HTMLButtonElement
>
)
=>
{
setAnchorEl
(
event
.
currentTarget
)
}
const
handleCloseMenu
=
()
=>
{
setAnchorEl
(
null
)
}
const
handleUnpinUser
=
()
=>
{
setIsPinned
(
false
)
setPinnedUser
(
null
)
setAnchorEl
(
null
)
}
useEffect
(()
=>
{
setItemHeight
((
gridContainerHeight
-
16
*
2
)
/
3
)
},
[
gridContainerHeight
])
return
(
<
Box
sx=
{
{
display
:
'
flex
'
,
width
:
'
100%
'
,
flexDirection
:
'
row
'
,
justifyContent
:
'
space-between
'
,
height
:
'
100%
'
,
}
}
>
<
Box
sx=
{
{
width
:
isSm
?
'
65%
'
:
'
100%
'
,
backgroundColor
:
'
#262625
'
,
height
:
'
100%
'
,
borderRadius
:
'
15px
'
,
marginX
:
'
10px
'
,
position
:
'
relative
'
,
}
}
>
<
RemoteUser
user=
{
pinnedUser
}
playVideo=
{
true
}
playAudio=
{
true
}
style=
{
{
width
:
'
100%
'
,
height
:
'
100%
'
,
borderRadius
:
'
15px
'
}
}
/>
<
IconButton
aria
-
lable=
'Menu'
sx=
{
{
position
:
'
absolute
'
,
top
:
'
5%
'
,
right
:
'
5%
'
,
borderRadius
:
'
50%
'
,
padding
:
'
10px
'
,
zIndex
:
100
,
pointerEvents
:
'
auto
'
,
'
&:hover
'
:
{
backgroundColor
:
'
#363739
'
,
opacity
:
0.8
},
}
}
onClick=
{
handleClickMenu
}
>
<
MoreVertIcon
/>
</
IconButton
>
<
Popover
id=
{
menuId
}
open=
{
open
}
anchorEl=
{
anchorEl
}
onClose=
{
handleCloseMenu
}
anchorOrigin=
{
{
vertical
:
'
bottom
'
,
horizontal
:
'
left
'
,
}
}
>
<
Box
sx=
{
{
padding
:
'
5px 20px
'
,
display
:
'
flex
'
,
flexDirection
:
'
columnÍ
'
,
backgroundColor
:
'
#363739
'
,
width
:
'
150px
'
,
}
}
>
<
Button
sx=
{
{
color
:
theme
.
palette
.
common
.
white
,
width
:
'
100%
'
,
textAlign
:
'
left
'
,
justifyContent
:
'
start
'
,
textTransform
:
'
none
'
,
}
}
onClick=
{
handleUnpinUser
}
>
Unpin User
</
Button
>
</
Box
>
</
Popover
>
</
Box
>
{
isSm
&&
(
<
Box
sx=
{
{
width
:
'
30%
'
,
display
:
'
flex
'
,
flexDirection
:
'
column
'
,
alignItems
:
'
center
'
,
justifyContent
:
'
center
'
,
height
:
'
100%
'
,
}
}
>
<
Stack
spacing=
{
4
}
sx=
{
{
width
:
'
100%
'
,
height
:
'
fit-content
'
}
}
>
<
UserCard
type=
{
'
local
'
}
itemHeight=
{
itemHeight
}
setIsPinned=
{
setIsPinned
}
setPinnedUser=
{
setPinnedUser
}
/>
{
remoteUsers
.
length
>
1
&&
remoteUsers
.
filter
((
user
)
=>
user
.
uid
!==
pinnedUser
.
uid
)
.
map
((
user
)
=>
(
<
UserCard
key=
{
user
.
uid
}
type=
{
'
remote
'
}
remoteUser=
{
user
}
itemHeight=
{
itemHeight
}
setIsPinned=
{
setIsPinned
}
setPinnedUser=
{
setPinnedUser
}
/>
))
}
</
Stack
>
</
Box
>
)
}
</
Box
>
)
}
export
default
PinnedGrid
src/Components/SymmetricGrid/SymmetricGrid.tsx
View file @
e6ce68d9
import
React
,
{
useEffect
,
useState
}
from
'
react
'
import
React
,
{
useEffect
,
useState
}
from
'
react
'
import
{
Box
,
Grid
}
from
'
@mui/material
'
import
{
Box
,
Grid
}
from
'
@mui/material
'
import
useMediaQuery
from
'
@mui/material/useMediaQuery
'
import
useMediaQuery
from
'
@mui/material/useMediaQuery
'
import
{
LocalVideoTrack
,
RemoteUser
,
useLocalCameraTrack
,
useRemoteUsers
}
from
'
agora-rtc-react
'
import
{
useRemoteUsers
}
from
'
agora-rtc-react
'
import
{
useTheme
,
type
Theme
}
from
'
@mui/material/styles
'
import
{
useTheme
,
type
Theme
}
from
'
@mui/material/styles
'
import
type
{
IAgoraRTCRemoteUser
}
from
'
agora-rtc-sdk-ng
'
import
UserCard
from
'
../UserCard/UserCard
'
interface
Props
{
interface
Props
{
totalUsers
:
number
totalUsers
:
number
setTotalUsers
:
(
value
:
number
)
=>
void
gridContainerHeight
:
number
gridContainerHeight
:
number
setGridContainerHeight
:
(
value
:
number
)
=>
void
isPinned
:
boolean
setIsPinned
:
(
value
:
boolean
)
=>
void
setPinnedUser
:
(
value
:
IAgoraRTCRemoteUser
|
null
)
=>
void
}
}
const
SymmetricGrid
=
({
const
SymmetricGrid
=
({
totalUsers
,
totalUsers
,
setTotalUsers
,
gridContainerHeight
,
gridContainerHeight
,
setGridContainerHeight
,
isPinned
,
setIsPinned
,
setPinnedUser
,
}:
Props
)
=>
{
}:
Props
)
=>
{
const
theme
:
Theme
=
useTheme
()
const
theme
:
Theme
=
useTheme
()
const
isSm
=
useMediaQuery
(
theme
.
breakpoints
.
up
(
'
sm
'
))
const
isSm
=
useMediaQuery
(
theme
.
breakpoints
.
up
(
'
sm
'
))
const
isMd
=
useMediaQuery
(
theme
.
breakpoints
.
up
(
'
md
'
))
const
isMd
=
useMediaQuery
(
theme
.
breakpoints
.
up
(
'
md
'
))
const
isLg
=
useMediaQuery
(
theme
.
breakpoints
.
up
(
'
lg
'
))
const
isLg
=
useMediaQuery
(
theme
.
breakpoints
.
up
(
'
lg
'
))
const
{
localCameraTrack
}
=
useLocalCameraTrack
()
const
remoteUsers
=
useRemoteUsers
()
const
remoteUsers
=
useRemoteUsers
()
const
[
itemOffset
,
setItemOffset
]
=
useState
(
12
)
const
[
itemOffset
,
setItemOffset
]
=
useState
(
12
)
...
@@ -31,10 +34,6 @@ const SymmetricGrid = ({
...
@@ -31,10 +34,6 @@ const SymmetricGrid = ({
const
maxUsersDisplay
=
isLg
?
12
:
isMd
?
9
:
isSm
?
4
:
2
const
maxUsersDisplay
=
isLg
?
12
:
isMd
?
9
:
isSm
?
4
:
2
useEffect
(()
=>
{
setTotalUsers
(
remoteUsers
.
length
+
1
)
},
[
remoteUsers
])
useEffect
(()
=>
{
useEffect
(()
=>
{
setNumOfItems
(
totalUsers
>
maxUsersDisplay
?
maxUsersDisplay
:
totalUsers
)
setNumOfItems
(
totalUsers
>
maxUsersDisplay
?
maxUsersDisplay
:
totalUsers
)
},
[
totalUsers
])
},
[
totalUsers
])
...
@@ -129,36 +128,22 @@ const SymmetricGrid = ({
...
@@ -129,36 +128,22 @@ const SymmetricGrid = ({
<
Box
sx=
{
{
display
:
'
flex
'
,
width
:
'
100%
'
}
}
>
<
Box
sx=
{
{
display
:
'
flex
'
,
width
:
'
100%
'
}
}
>
<
Grid
container
spacing=
{
3
}
width=
{
'
100%
'
}
padding=
{
0
}
>
<
Grid
container
spacing=
{
3
}
width=
{
'
100%
'
}
padding=
{
0
}
>
<
Grid
item
xs=
{
itemOffset
}
>
<
Grid
item
xs=
{
itemOffset
}
>
<
Box
<
UserCard
sx=
{
{
type=
{
'
local
'
}
backgroundColor
:
'
#262625
'
,
itemHeight=
{
itemHeight
}
height
:
`${itemHeight}px`
,
setIsPinned=
{
setIsPinned
}
borderRadius
:
'
15px
'
,
setPinnedUser=
{
setPinnedUser
}
}
}
/>
>
<
LocalVideoTrack
track=
{
localCameraTrack
}
play=
{
true
}
style=
{
{
width
:
'
100%
'
,
height
:
'
100%
'
,
borderRadius
:
'
20%
'
}
}
/>
</
Box
>
</
Grid
>
</
Grid
>
{
remoteUsers
.
map
((
remoteUser
,
index
)
=>
(
{
remoteUsers
.
map
((
remoteUser
,
index
)
=>
(
<
Grid
item
xs=
{
itemOffset
}
key=
{
index
}
>
<
Grid
item
xs=
{
itemOffset
}
key=
{
index
}
>
<
Box
<
UserCard
sx=
{
{
type=
{
'
remote
'
}
backgroundColor
:
'
#262625
'
,
remoteUser=
{
remoteUser
}
height
:
`${itemHeight}px`
,
itemHeight=
{
itemHeight
}
borderRadius
:
'
15px
'
,
setIsPinned=
{
setIsPinned
}
}
}
setPinnedUser=
{
setPinnedUser
}
>
/>
<
RemoteUser
user=
{
remoteUser
}
playVideo=
{
true
}
playAudio=
{
true
}
style=
{
{
width
:
'
100%
'
,
height
:
'
100%
'
,
borderRadius
:
'
15px
'
}
}
/>
</
Box
>
</
Grid
>
</
Grid
>
))
}
))
}
</
Grid
>
</
Grid
>
...
...
src/Components/UserCard/UserCard.tsx
0 → 100644
View file @
e6ce68d9
import
{
Box
,
Button
,
IconButton
,
Popover
}
from
'
@mui/material
'
import
MoreVertIcon
from
'
@mui/icons-material/MoreVert
'
import
{
LocalVideoTrack
,
RemoteUser
,
useLocalCameraTrack
}
from
'
agora-rtc-react
'
import
type
{
IAgoraRTCRemoteUser
}
from
'
agora-rtc-sdk-ng
'
import
{
useTheme
,
type
Theme
}
from
'
@mui/material/styles
'
import
React
,
{
useEffect
}
from
'
react
'
interface
Props
{
type
:
'
remote
'
|
'
local
'
itemHeight
:
number
setIsPinned
:
(
value
:
boolean
)
=>
void
setPinnedUser
:
(
value
:
IAgoraRTCRemoteUser
|
null
)
=>
void
remoteUser
?:
IAgoraRTCRemoteUser
}
const
UserCard
=
({
type
,
remoteUser
,
itemHeight
,
setIsPinned
,
setPinnedUser
}:
Props
)
=>
{
const
theme
:
Theme
=
useTheme
()
const
{
localCameraTrack
}
=
useLocalCameraTrack
()
const
[
anchorEl
,
setAnchorEl
]
=
React
.
useState
<
HTMLButtonElement
|
null
>
(
null
)
const
[
isLocal
,
setIsLocal
]
=
React
.
useState
<
boolean
>
(
type
===
'
local
'
)
const
open
=
Boolean
(
anchorEl
)
const
menuId
=
open
?
'
simple-popover
'
:
undefined
useEffect
(()
=>
{
setIsLocal
(
type
===
'
local
'
)
},
[
type
])
const
handleClickMenu
=
(
event
:
React
.
MouseEvent
<
HTMLButtonElement
>
)
=>
{
setAnchorEl
(
event
.
currentTarget
)
}
const
handleCloseMenu
=
()
=>
{
setAnchorEl
(
null
)
}
const
handlePinUser
=
()
=>
{
setIsPinned
(
true
)
setPinnedUser
(
remoteUser
??
null
)
setAnchorEl
(
null
)
}
return
(
<
Box
sx=
{
{
backgroundColor
:
'
#262625
'
,
height
:
`${itemHeight}px`
,
borderRadius
:
'
15px
'
,
width
:
'
100%
'
,
position
:
'
relative
'
,
overflow
:
'
hidden
'
,
}
}
>
{
isLocal
?
(
<
LocalVideoTrack
track=
{
localCameraTrack
}
play=
{
true
}
style=
{
{
width
:
'
100%
'
,
height
:
'
100%
'
,
borderRadius
:
'
20%
'
}
}
/>
)
:
(
<
RemoteUser
user=
{
remoteUser
}
playVideo=
{
true
}
playAudio=
{
true
}
style=
{
{
width
:
'
100%
'
,
height
:
'
100%
'
,
borderRadius
:
'
15px
'
}
}
/>
)
}
{
!
isLocal
&&
(
<>
<
IconButton
aria
-
lable=
'Menu'
sx=
{
{
position
:
'
absolute
'
,
top
:
'
5%
'
,
right
:
'
5%
'
,
borderRadius
:
'
50%
'
,
padding
:
'
10px
'
,
zIndex
:
100
,
pointerEvents
:
'
auto
'
,
'
&:hover
'
:
{
backgroundColor
:
'
#363739
'
,
opacity
:
0.8
},
}
}
onClick=
{
handleClickMenu
}
>
<
MoreVertIcon
/>
</
IconButton
>
<
Popover
id=
{
menuId
}
open=
{
open
}
anchorEl=
{
anchorEl
}
onClose=
{
handleCloseMenu
}
anchorOrigin=
{
{
vertical
:
'
bottom
'
,
horizontal
:
'
left
'
,
}
}
>
<
Box
sx=
{
{
padding
:
'
5px 20px
'
,
display
:
'
flex
'
,
flexDirection
:
'
columnÍ
'
,
backgroundColor
:
'
#363739
'
,
width
:
'
150px
'
,
}
}
>
<
Button
sx=
{
{
color
:
theme
.
palette
.
common
.
white
,
width
:
'
100%
'
,
textAlign
:
'
left
'
,
justifyContent
:
'
start
'
,
textTransform
:
'
none
'
,
}
}
onClick=
{
handlePinUser
}
>
Pin User
</
Button
>
</
Box
>
</
Popover
>
</>
)
}
</
Box
>
)
}
export
default
UserCard
src/Services/AgoraManager/AgoraManager.tsx
View file @
e6ce68d9
...
@@ -5,7 +5,7 @@ import type { IMicrophoneAudioTrack, ICameraVideoTrack } from 'agora-rtc-sdk-ng'
...
@@ -5,7 +5,7 @@ import type { IMicrophoneAudioTrack, ICameraVideoTrack } from 'agora-rtc-sdk-ng'
import
{
Box
}
from
'
@mui/material
'
import
{
Box
}
from
'
@mui/material
'
import
{
useTheme
,
type
Theme
}
from
'
@mui/material/styles
'
import
{
useTheme
,
type
Theme
}
from
'
@mui/material/styles
'
import
MeetingLoading
from
'
../../Components/MeetingLoading/MeetingLoading
'
import
MeetingLoading
from
'
../../Components/MeetingLoading/MeetingLoading
'
import
Meeting
Grid
from
'
../../Components/MeetingLayout/MeetingLayout
'
import
Meeting
Layout
from
'
../../Components/MeetingLayout/MeetingLayout
'
interface
AgoraContextType
{
interface
AgoraContextType
{
localCameraTrack
:
ICameraVideoTrack
|
null
localCameraTrack
:
ICameraVideoTrack
|
null
...
@@ -73,7 +73,7 @@ export const AgoraManager = ({ children }: { children: React.ReactNode }) => {
...
@@ -73,7 +73,7 @@ export const AgoraManager = ({ children }: { children: React.ReactNode }) => {
localCameraTrack=
{
localCameraTrack
}
localCameraTrack=
{
localCameraTrack
}
localMicrophoneTrack=
{
localMicrophoneTrack
}
localMicrophoneTrack=
{
localMicrophoneTrack
}
>
>
<
Meeting
Grid
/>
<
Meeting
Layout
/>
</
AgoraProvider
>
</
AgoraProvider
>
)
:
joinError
?
(
)
:
joinError
?
(
<
MeetingLoading
variant=
'error'
/>
<
MeetingLoading
variant=
'error'
/>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment