mirror of
https://github.com/filecoin-project/slate.git
synced 2024-11-27 01:03:08 +03:00
Merge pull request #503 from filecoin-project/@akuokojnr/multiplayer-slate
feat: show user online status
This commit is contained in:
commit
21fba66ef0
@ -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) => {
|
||||||
|
@ -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 = {
|
||||||
|
@ -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;
|
||||||
|
@ -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 && (
|
||||||
|
@ -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>
|
||||||
|
@ -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",
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user