CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/account/avatar/users-viewing.tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { useInterval } from "react-interval-hook";
7
8
import {
9
CSS,
10
redux,
11
useMemo,
12
useState,
13
useTypedRedux,
14
} from "@cocalc/frontend/app-framework";
15
import { Loading } from "@cocalc/frontend/components";
16
import { cmp } from "@cocalc/util/misc";
17
import { Avatar } from "./avatar";
18
19
// How frequently all UsersViewing componenents are completely updated.
20
// This is only needed to ensure that faces fade out; any newly added faces
21
// will still be displayed instantly. Also, updating more frequently updates
22
// the line positions in the tooltip.
23
const UPDATE_INTERVAL_S = 15;
24
25
// Cutoff for how recent activity must be to show users. Should be significantly
26
// longer than default for the mark_file function in the file_use actions.
27
const MAX_AGE_S = 600;
28
29
interface Activity {
30
project_id: string;
31
path: string;
32
last_used: Date;
33
}
34
35
function most_recent(activity: Activity[]): Activity {
36
if (activity.length == 0) throw Error("must have some activity");
37
let { last_used } = activity[0];
38
let y = activity[0];
39
for (let x of activity.slice(1)) {
40
if (x.last_used >= last_used) {
41
y = x;
42
({ last_used } = x);
43
}
44
}
45
return y;
46
}
47
48
const USERS_VIEWING_STYLE: React.CSSProperties = {
49
overflowX: "auto",
50
overflowY: "hidden",
51
zIndex: 1,
52
whiteSpace: "nowrap",
53
padding: "1px", // if not set, Chrome draws scrollbars around it #5399
54
height: "32px",
55
} as const;
56
57
const DEFAULT_STYLE: CSS = { maxWidth: "120px" } as const;
58
59
// If neither project_id nor path given, then viewing all projects; if project_id
60
// given, then viewing that project; if both given, then viewing a particular file.
61
interface Props {
62
project_id?: string; // optional -- must be given if path is specified
63
path?: string; // optional -- if given, viewing a file.
64
max_age_s?: number;
65
size?: number;
66
style?: React.CSSProperties;
67
}
68
69
function useUsersViewing(
70
project_id?: string,
71
path?: string,
72
max_age_s?: number,
73
) {
74
const [counter, set_counter] = useState(0); // used to force update periodically.
75
76
// only so component is updated immediately whenever file use changes
77
const file_use = useTypedRedux("file_use", "file_use");
78
const users = useMemo(
79
() =>
80
redux.getStore("file_use")?.get_active_users({
81
project_id,
82
path,
83
max_age_s,
84
}),
85
[file_use, project_id, path, max_age_s],
86
);
87
88
useInterval(() => {
89
// cause an update
90
set_counter(counter + 1);
91
}, UPDATE_INTERVAL_S * 1000);
92
93
return { users, file_use };
94
}
95
96
export function UsersViewing(props: Readonly<Props>) {
97
const {
98
path,
99
project_id,
100
max_age_s = MAX_AGE_S,
101
style = DEFAULT_STYLE,
102
size = 24,
103
} = props;
104
105
const { users, file_use } = useUsersViewing(project_id, path, max_age_s);
106
107
// so we can exclude ourselves from list of faces
108
const our_account_id: string | undefined = useTypedRedux(
109
"account",
110
"account_id",
111
);
112
113
function render_active_users(users) {
114
const v: {
115
account_id: string;
116
activity: Activity;
117
}[] = [];
118
if (users != null) {
119
for (const account_id in users) {
120
const activity = users[account_id];
121
if (!activity || activity.length == 0) {
122
continue; // shouldn't happen, but just be extra careful
123
}
124
v.push({ account_id, activity: most_recent(activity) });
125
}
126
}
127
v.sort((a, b) => cmp(b.activity.last_used, a.activity.last_used));
128
let i = 0;
129
const r: JSX.Element[] = [];
130
for (const { account_id, activity } of v) {
131
// only show other users
132
if (account_id !== our_account_id) {
133
i += 1;
134
r.push(
135
<Avatar
136
key={account_id + i}
137
account_id={account_id}
138
max_age_s={max_age_s}
139
project_id={project_id}
140
path={path}
141
size={size}
142
activity={activity}
143
/>,
144
);
145
}
146
}
147
return r;
148
}
149
150
if (file_use == null || our_account_id == null) {
151
return <Loading />;
152
}
153
154
return (
155
<div style={{ ...USERS_VIEWING_STYLE, ...style }}>
156
{render_active_users(users)}
157
</div>
158
);
159
}
160
161