diff --git a/common/browser-websockets.js b/common/browser-websockets.js index 7c7d02f9..2342dac1 100644 --- a/common/browser-websockets.js +++ b/common/browser-websockets.js @@ -8,7 +8,7 @@ let savedResource = null; let savedViewer = null; let savedOnUpdate = null; -export const init = ({ resource = "", viewer, onUpdate }) => { +export const init = ({ resource = "", viewer, onUpdate, onNewActiveUser }) => { savedResource = resource; savedViewer = viewer; savedOnUpdate = onUpdate; @@ -78,6 +78,10 @@ export const init = ({ resource = "", viewer, onUpdate }) => { if (type === "UPDATE") { onUpdate(data); } + + if (type === "UPDATE_USERS_ONLINE") { + onNewActiveUser(data); + } }); client.addEventListener("close", (e) => { diff --git a/common/constants.js b/common/constants.js index d541afc8..55af9fb4 100644 --- a/common/constants.js +++ b/common/constants.js @@ -57,6 +57,7 @@ export const system = { white: "#FFFFFF", bgBlurGrayBlack: "rgba(15, 14, 18, 0.8)", bgBlurBlack: "rgba(15, 14, 18, 0.9)", + active: "#00BB00", }; export const shadow = { diff --git a/components/core/Application.js b/components/core/Application.js index e21234ac..2d7eadd6 100644 --- a/components/core/Application.js +++ b/components/core/Application.js @@ -105,6 +105,7 @@ export default class ApplicationPage extends React.Component { online: null, mobile: this.props.mobile, loaded: false, + activeUsers: null, }; async componentDidMount() { @@ -209,6 +210,7 @@ export default class ApplicationPage extends React.Component { resource: this.props.resources.pubsub, viewer: this.state.viewer, onUpdate: this._handleUpdateViewer, + onNewActiveUser: this._handleNewActiveUser, }); } if (!wsclient) { @@ -220,6 +222,10 @@ export default class ApplicationPage extends React.Component { return; }; + _handleNewActiveUser = (users) => { + this.setState({ activeUsers: users }); + }; + _handleWindowResize = () => { const { width } = Window.getViewportSize(); @@ -631,6 +637,7 @@ export default class ApplicationPage extends React.Component { sceneId: current.target.id, mobile: this.state.mobile, resources: this.props.resources, + activeUsers: this.state.activeUsers, }); let sidebarElement; diff --git a/components/core/Profile.js b/components/core/Profile.js index ee9be700..2ab9e4ef 100644 --- a/components/core/Profile.js +++ b/components/core/Profile.js @@ -71,12 +71,24 @@ const STYLES_PROFILE_IMAGE = css` flex-shrink: 0; border-radius: 4px; margin: 0 auto; + position: relative; @media (max-width: ${Constants.sizes.mobile}px) { width: 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` font-size: ${Constants.typescale.lvl4}; font-family: ${Constants.font.semiBold}; @@ -183,6 +195,18 @@ const STYLES_DIRECTORY_PROFILE_IMAGE = css` width: 24px; margin-right: 16px; 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` @@ -208,7 +232,16 @@ const STYLES_COPY_INPUT = css` opacity: 0; `; -function UserEntry({ user, button, onClick, message, external, url }) { +function UserEntry({ + user, + button, + onClick, + message, + external, + url, + userOnline, + showStatusIndicator, +}) { return (
{external ? ( @@ -216,7 +249,17 @@ function UserEntry({ user, button, onClick, message, external, url }) {
+ > + {showStatusIndicator && ( +
+ )} +
{user.data.name || `@${user.username}`} {message ? {message} : null} @@ -227,7 +270,15 @@ function UserEntry({ user, button, onClick, message, external, url }) {
+ > +
+
{user.data.name || `@${user.username}`} {message ? {message} : null} @@ -259,11 +310,13 @@ export default class Profile extends React.Component { }).length, fetched: false, tab: this.props.tab, + isOnline: false, }; componentDidMount = () => { this._handleUpdatePage(); this.filterByVisibility(); + this.checkStatus(); }; 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() { let tab = typeof this.state.tab === "undefined" || this.state.tab === null ? 1 : this.state.tab; let isOwner = this.props.isOwner; @@ -419,6 +479,8 @@ export default class Profile extends React.Component { key={relation.id} user={relation.user} button={button} + userOnline={this.state.isOnline} + showStatusIndicator={this.props.isAuthenticated} onClick={() => { this.props.onAction({ type: "NAVIGATE", @@ -471,6 +533,8 @@ export default class Profile extends React.Component { key={relation.id} user={relation.owner} button={button} + userOnline={this.state.isOnline} + showStatusIndicator={this.props.isAuthenticated} onClick={() => { this.props.onAction({ type: "NAVIGATE", @@ -491,6 +555,8 @@ export default class Profile extends React.Component { return total + slate.data?.objects?.length || 0; }, 0); + const showStatusIndicator = this.props.isAuthenticated; + return (
+ style={{ + backgroundImage: `url('${creator.data.photo}')`, + }} + > + {showStatusIndicator && ( +
+ )} +
{Strings.getPresentationName(creator)}
{!isOwner && ( diff --git a/pages/_/profile.js b/pages/_/profile.js index f318aa96..4b66bea8 100644 --- a/pages/_/profile.js +++ b/pages/_/profile.js @@ -68,6 +68,7 @@ export default class ProfilePage extends React.Component { page={this.state.page} buttons={buttons} isOwner={false} + isAuthenticated={this.props.viewer !== null} external />
diff --git a/scenes/SceneDirectory.js b/scenes/SceneDirectory.js index 1c699e08..cdb3d013 100644 --- a/scenes/SceneDirectory.js +++ b/scenes/SceneDirectory.js @@ -81,6 +81,18 @@ const STYLES_PROFILE_IMAGE = css` width: 24px; margin-right: 16px; 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` @@ -100,11 +112,19 @@ const STYLES_NAME = css` text-overflow: ellipsis; `; -function UserEntry({ user, button, onClick, message }) { +function UserEntry({ user, button, onClick, message, userOnline }) { return (
-
+
+
+
{user.data.name || `@${user.username}`} {message ? {message} : null} @@ -139,6 +159,11 @@ export default class SceneDirectory extends React.Component { state = { copyValue: "", contextMenu: null, + isOnline: false, + }; + + componentDidMount = () => { + this.checkStatus(); }; _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() { let following = this.props.viewer.subscriptions .filter((relation) => { @@ -208,6 +240,7 @@ export default class SceneDirectory extends React.Component { key={relation.id} user={relation.user} button={button} + userOnline={this.state.isOnline} onClick={() => { this.props.onAction({ type: "NAVIGATE", @@ -256,6 +289,7 @@ export default class SceneDirectory extends React.Component { key={relation.id} user={relation.owner} button={button} + userOnline={this.state.isOnline} onClick={() => { this.props.onAction({ type: "NAVIGATE", diff --git a/scenes/SceneProfile.js b/scenes/SceneProfile.js index d0386e1a..ea29fcb1 100644 --- a/scenes/SceneProfile.js +++ b/scenes/SceneProfile.js @@ -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 } isOwner={this.state.profile.id === this.props.viewer.id} + isAuthenticated={this.props.viewer !== null} key={this.state.profile.id} /> );