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}
/>
);