Merge pull request #503 from filecoin-project/@akuokojnr/multiplayer-slate

feat: show user online status
This commit is contained in:
CAKE 2021-02-05 22:22:00 -08:00 committed by GitHub
commit 21fba66ef0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 134 additions and 8 deletions

View File

@ -8,7 +8,7 @@ let savedResource = null;
let savedViewer = null; let savedViewer = null;
let savedOnUpdate = null; let savedOnUpdate = null;
export const init = ({ resource = "", viewer, onUpdate }) => { export const init = ({ resource = "", viewer, onUpdate, onNewActiveUser }) => {
savedResource = resource; savedResource = resource;
savedViewer = viewer; savedViewer = viewer;
savedOnUpdate = onUpdate; savedOnUpdate = onUpdate;
@ -78,6 +78,10 @@ export const init = ({ resource = "", viewer, onUpdate }) => {
if (type === "UPDATE") { if (type === "UPDATE") {
onUpdate(data); onUpdate(data);
} }
if (type === "UPDATE_USERS_ONLINE") {
onNewActiveUser(data);
}
}); });
client.addEventListener("close", (e) => { client.addEventListener("close", (e) => {

View File

@ -57,6 +57,7 @@ export const system = {
white: "#FFFFFF", white: "#FFFFFF",
bgBlurGrayBlack: "rgba(15, 14, 18, 0.8)", bgBlurGrayBlack: "rgba(15, 14, 18, 0.8)",
bgBlurBlack: "rgba(15, 14, 18, 0.9)", bgBlurBlack: "rgba(15, 14, 18, 0.9)",
active: "#00BB00",
}; };
export const shadow = { export const shadow = {

View File

@ -105,6 +105,7 @@ export default class ApplicationPage extends React.Component {
online: null, online: null,
mobile: this.props.mobile, mobile: this.props.mobile,
loaded: false, loaded: false,
activeUsers: null,
}; };
async componentDidMount() { async componentDidMount() {
@ -209,6 +210,7 @@ export default class ApplicationPage extends React.Component {
resource: this.props.resources.pubsub, resource: this.props.resources.pubsub,
viewer: this.state.viewer, viewer: this.state.viewer,
onUpdate: this._handleUpdateViewer, onUpdate: this._handleUpdateViewer,
onNewActiveUser: this._handleNewActiveUser,
}); });
} }
if (!wsclient) { if (!wsclient) {
@ -220,6 +222,10 @@ export default class ApplicationPage extends React.Component {
return; return;
}; };
_handleNewActiveUser = (users) => {
this.setState({ activeUsers: users });
};
_handleWindowResize = () => { _handleWindowResize = () => {
const { width } = Window.getViewportSize(); const { width } = Window.getViewportSize();
@ -631,6 +637,7 @@ export default class ApplicationPage extends React.Component {
sceneId: current.target.id, sceneId: current.target.id,
mobile: this.state.mobile, mobile: this.state.mobile,
resources: this.props.resources, resources: this.props.resources,
activeUsers: this.state.activeUsers,
}); });
let sidebarElement; let sidebarElement;

View File

@ -71,12 +71,24 @@ const STYLES_PROFILE_IMAGE = css`
flex-shrink: 0; flex-shrink: 0;
border-radius: 4px; border-radius: 4px;
margin: 0 auto; margin: 0 auto;
position: relative;
@media (max-width: ${Constants.sizes.mobile}px) { @media (max-width: ${Constants.sizes.mobile}px) {
width: 64px; width: 64px;
height: 64px; height: 64px;
} }
`; `;
const STYLES_STATUS_INDICATOR = css`
position: absolute;
bottom: 0;
right: 0;
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid ${Constants.system.gray50};
background-color: ${Constants.system.white};
`;
const STYLES_NAME = css` const STYLES_NAME = css`
font-size: ${Constants.typescale.lvl4}; font-size: ${Constants.typescale.lvl4};
font-family: ${Constants.font.semiBold}; font-family: ${Constants.font.semiBold};
@ -183,6 +195,18 @@ const STYLES_DIRECTORY_PROFILE_IMAGE = css`
width: 24px; width: 24px;
margin-right: 16px; margin-right: 16px;
border-radius: 4px; border-radius: 4px;
position: relative;
`;
const STYLES_DIRECTORY_STATUS_INDICATOR = css`
position: absolute;
bottom: 0;
right: 0;
width: 7px;
height: 7px;
border-radius: 50%;
border: 1.2px solid ${Constants.system.gray50};
background-color: ${Constants.system.white};
`; `;
const STYLES_MESSAGE = css` const STYLES_MESSAGE = css`
@ -208,7 +232,16 @@ const STYLES_COPY_INPUT = css`
opacity: 0; opacity: 0;
`; `;
function UserEntry({ user, button, onClick, message, external, url }) { function UserEntry({
user,
button,
onClick,
message,
external,
url,
userOnline,
showStatusIndicator,
}) {
return ( return (
<div key={user.username} css={STYLES_USER_ENTRY}> <div key={user.username} css={STYLES_USER_ENTRY}>
{external ? ( {external ? (
@ -216,7 +249,17 @@ function UserEntry({ user, button, onClick, message, external, url }) {
<div <div
css={STYLES_DIRECTORY_PROFILE_IMAGE} css={STYLES_DIRECTORY_PROFILE_IMAGE}
style={{ backgroundImage: `url(${user.data.photo})` }} style={{ backgroundImage: `url(${user.data.photo})` }}
/> >
{showStatusIndicator && (
<div
css={STYLES_DIRECTORY_STATUS_INDICATOR}
style={{
borderColor: userOnline && `${Constants.system.active}`,
backgroundColor: userOnline && `${Constants.system.active}`,
}}
/>
)}
</div>
<span css={STYLES_DIRECTORY_NAME}> <span css={STYLES_DIRECTORY_NAME}>
{user.data.name || `@${user.username}`} {user.data.name || `@${user.username}`}
{message ? <span css={STYLES_MESSAGE}>{message}</span> : null} {message ? <span css={STYLES_MESSAGE}>{message}</span> : null}
@ -227,7 +270,15 @@ function UserEntry({ user, button, onClick, message, external, url }) {
<div <div
css={STYLES_DIRECTORY_PROFILE_IMAGE} css={STYLES_DIRECTORY_PROFILE_IMAGE}
style={{ backgroundImage: `url(${user.data.photo})` }} style={{ backgroundImage: `url(${user.data.photo})` }}
/> >
<div
css={STYLES_DIRECTORY_STATUS_INDICATOR}
style={{
borderColor: userOnline && `${Constants.system.active}`,
backgroundColor: userOnline && `${Constants.system.active}`,
}}
/>
</div>
<span css={STYLES_DIRECTORY_NAME}> <span css={STYLES_DIRECTORY_NAME}>
{user.data.name || `@${user.username}`} {user.data.name || `@${user.username}`}
{message ? <span css={STYLES_MESSAGE}>{message}</span> : null} {message ? <span css={STYLES_MESSAGE}>{message}</span> : null}
@ -259,11 +310,13 @@ export default class Profile extends React.Component {
}).length, }).length,
fetched: false, fetched: false,
tab: this.props.tab, tab: this.props.tab,
isOnline: false,
}; };
componentDidMount = () => { componentDidMount = () => {
this._handleUpdatePage(); this._handleUpdatePage();
this.filterByVisibility(); this.filterByVisibility();
this.checkStatus();
}; };
componentDidUpdate = (prevProps) => { componentDidUpdate = (prevProps) => {
@ -352,6 +405,13 @@ export default class Profile extends React.Component {
}); });
}; };
checkStatus = () => {
const activeUsers = this.props.activeUsers;
const userId = this.props.data?.id;
this.setState({ isOnline: activeUsers && activeUsers.includes(userId) });
};
render() { render() {
let tab = typeof this.state.tab === "undefined" || this.state.tab === null ? 1 : this.state.tab; let tab = typeof this.state.tab === "undefined" || this.state.tab === null ? 1 : this.state.tab;
let isOwner = this.props.isOwner; let isOwner = this.props.isOwner;
@ -419,6 +479,8 @@ export default class Profile extends React.Component {
key={relation.id} key={relation.id}
user={relation.user} user={relation.user}
button={button} button={button}
userOnline={this.state.isOnline}
showStatusIndicator={this.props.isAuthenticated}
onClick={() => { onClick={() => {
this.props.onAction({ this.props.onAction({
type: "NAVIGATE", type: "NAVIGATE",
@ -471,6 +533,8 @@ export default class Profile extends React.Component {
key={relation.id} key={relation.id}
user={relation.owner} user={relation.owner}
button={button} button={button}
userOnline={this.state.isOnline}
showStatusIndicator={this.props.isAuthenticated}
onClick={() => { onClick={() => {
this.props.onAction({ this.props.onAction({
type: "NAVIGATE", type: "NAVIGATE",
@ -491,6 +555,8 @@ export default class Profile extends React.Component {
return total + slate.data?.objects?.length || 0; return total + slate.data?.objects?.length || 0;
}, 0); }, 0);
const showStatusIndicator = this.props.isAuthenticated;
return ( return (
<div> <div>
<GlobalCarousel <GlobalCarousel
@ -508,8 +574,20 @@ export default class Profile extends React.Component {
<div css={STYLES_PROFILE_INFO}> <div css={STYLES_PROFILE_INFO}>
<div <div
css={STYLES_PROFILE_IMAGE} css={STYLES_PROFILE_IMAGE}
style={{ backgroundImage: `url('${creator.data.photo}')` }} style={{
/> backgroundImage: `url('${creator.data.photo}')`,
}}
>
{showStatusIndicator && (
<div
css={STYLES_STATUS_INDICATOR}
style={{
borderColor: this.state.isOnline && `${Constants.system.active}`,
backgroundColor: this.state.isOnline && `${Constants.system.active}`,
}}
/>
)}
</div>
<div css={STYLES_INFO}> <div css={STYLES_INFO}>
<div css={STYLES_NAME}>{Strings.getPresentationName(creator)}</div> <div css={STYLES_NAME}>{Strings.getPresentationName(creator)}</div>
{!isOwner && ( {!isOwner && (

View File

@ -68,6 +68,7 @@ export default class ProfilePage extends React.Component {
page={this.state.page} page={this.state.page}
buttons={buttons} buttons={buttons}
isOwner={false} isOwner={false}
isAuthenticated={this.props.viewer !== null}
external external
/> />
</div> </div>

View File

@ -81,6 +81,18 @@ const STYLES_PROFILE_IMAGE = css`
width: 24px; width: 24px;
margin-right: 16px; margin-right: 16px;
border-radius: 4px; border-radius: 4px;
position: relative;
`;
const STYLES_STATUS_INDICATOR = css`
position: absolute;
bottom: 0;
right: 0;
width: 7px;
height: 7px;
border-radius: 50%;
border: 2px solid ${Constants.system.gray50};
background-color: ${Constants.system.white};
`; `;
const STYLES_MESSAGE = css` const STYLES_MESSAGE = css`
@ -100,11 +112,19 @@ const STYLES_NAME = css`
text-overflow: ellipsis; text-overflow: ellipsis;
`; `;
function UserEntry({ user, button, onClick, message }) { function UserEntry({ user, button, onClick, message, userOnline }) {
return ( return (
<div key={user.username} css={STYLES_USER_ENTRY}> <div key={user.username} css={STYLES_USER_ENTRY}>
<div css={STYLES_USER} onClick={onClick}> <div css={STYLES_USER} onClick={onClick}>
<div css={STYLES_PROFILE_IMAGE} style={{ backgroundImage: `url(${user.data.photo})` }} /> <div css={STYLES_PROFILE_IMAGE} style={{ backgroundImage: `url(${user.data.photo})` }}>
<div
css={STYLES_STATUS_INDICATOR}
style={{
borderColor: userOnline && `${Constants.system.active}`,
backgroundColor: userOnline && `${Constants.system.active}`,
}}
/>
</div>
<span css={STYLES_NAME}> <span css={STYLES_NAME}>
{user.data.name || `@${user.username}`} {user.data.name || `@${user.username}`}
{message ? <span css={STYLES_MESSAGE}>{message}</span> : null} {message ? <span css={STYLES_MESSAGE}>{message}</span> : null}
@ -139,6 +159,11 @@ export default class SceneDirectory extends React.Component {
state = { state = {
copyValue: "", copyValue: "",
contextMenu: null, contextMenu: null,
isOnline: false,
};
componentDidMount = () => {
this.checkStatus();
}; };
_handleCopy = (e, value) => { _handleCopy = (e, value) => {
@ -171,6 +196,13 @@ export default class SceneDirectory extends React.Component {
}); });
}; };
checkStatus = () => {
const activeUsers = this.props.activeUsers;
const userId = this.props.data?.id;
this.setState({ isOnline: activeUsers && activeUsers.includes(userId) });
};
render() { render() {
let following = this.props.viewer.subscriptions let following = this.props.viewer.subscriptions
.filter((relation) => { .filter((relation) => {
@ -208,6 +240,7 @@ export default class SceneDirectory extends React.Component {
key={relation.id} key={relation.id}
user={relation.user} user={relation.user}
button={button} button={button}
userOnline={this.state.isOnline}
onClick={() => { onClick={() => {
this.props.onAction({ this.props.onAction({
type: "NAVIGATE", type: "NAVIGATE",
@ -256,6 +289,7 @@ export default class SceneDirectory extends React.Component {
key={relation.id} key={relation.id}
user={relation.owner} user={relation.owner}
button={button} button={button}
userOnline={this.state.isOnline}
onClick={() => { onClick={() => {
this.props.onAction({ this.props.onAction({
type: "NAVIGATE", type: "NAVIGATE",

View File

@ -103,6 +103,7 @@ export default class SceneProfile extends React.Component {
this.state.profile.id === this.props.viewer.id ? this.props.viewer : this.state.profile this.state.profile.id === this.props.viewer.id ? this.props.viewer : this.state.profile
} }
isOwner={this.state.profile.id === this.props.viewer.id} isOwner={this.state.profile.id === this.props.viewer.id}
isAuthenticated={this.props.viewer !== null}
key={this.state.profile.id} key={this.state.profile.id}
/> />
); );