link-sharing.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. import { useCallback, useState, useEffect } from 'react'
  2. import { useTranslation } from 'react-i18next'
  3. import { useShareProjectContext } from './share-project-modal'
  4. import { setPublicAccessLevel } from '../utils/api'
  5. import { CopyToClipboard } from '@/shared/components/copy-to-clipboard'
  6. import { useProjectContext } from '@/shared/context/project-context'
  7. import * as eventTracking from '../../../infrastructure/event-tracking'
  8. import { useUserContext } from '@/shared/context/user-context'
  9. import { sendMB } from '../../../infrastructure/event-tracking'
  10. import { getJSON } from '../../../infrastructure/fetch-json'
  11. import useAbortController from '@/shared/hooks/use-abort-controller'
  12. import { debugConsole } from '@/utils/debugging'
  13. import getMeta from '@/utils/meta'
  14. import OLRow from '@/features/ui/components/ol/ol-row'
  15. import OLCol from '@/features/ui/components/ol/ol-col'
  16. import OLButton from '@/features/ui/components/ol/ol-button'
  17. import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
  18. import MaterialIcon from '@/shared/components/material-icon'
  19. type Tokens = {
  20. readAndWrite: string
  21. readAndWriteHashPrefix: string
  22. readAndWritePrefix: string
  23. readOnly: string
  24. readOnlyHashPrefix: string
  25. }
  26. type AccessLevel = 'private' | 'tokenBased' | 'readAndWrite' | 'readOnly'
  27. export default function LinkSharing() {
  28. const [inflight, setInflight] = useState(false)
  29. const [showLinks, setShowLinks] = useState(true)
  30. const { monitorRequest } = useShareProjectContext()
  31. const { _id: projectId, publicAccessLevel } = useProjectContext()
  32. // set the access level of a project
  33. const setAccessLevel = useCallback(
  34. (newPublicAccessLevel: string) => {
  35. setInflight(true)
  36. sendMB('link-sharing-click-off', {
  37. project_id: projectId,
  38. })
  39. monitorRequest(() =>
  40. setPublicAccessLevel(projectId, newPublicAccessLevel)
  41. )
  42. .then(() => {
  43. // NOTE: not calling `updateProject` here as it receives data via
  44. // project:publicAccessLevel:changed over the websocket connection
  45. // TODO: eventTracking.sendMB('project-make-token-based') when newPublicAccessLevel is 'tokenBased'
  46. })
  47. .finally(() => {
  48. setInflight(false)
  49. })
  50. },
  51. [monitorRequest, projectId]
  52. )
  53. switch (publicAccessLevel) {
  54. // Private (with token-access available)
  55. case 'private':
  56. return (
  57. <PrivateSharing
  58. setAccessLevel={setAccessLevel}
  59. inflight={inflight}
  60. projectId={projectId}
  61. setShowLinks={setShowLinks}
  62. />
  63. )
  64. // Token-based access
  65. case 'tokenBased':
  66. return (
  67. <TokenBasedSharing
  68. setAccessLevel={setAccessLevel}
  69. inflight={inflight}
  70. setShowLinks={setShowLinks}
  71. showLinks={showLinks}
  72. />
  73. )
  74. // Legacy public-access
  75. case 'readAndWrite':
  76. case 'readOnly':
  77. return (
  78. // TODO: do we even need this anymore?
  79. <LegacySharing
  80. setAccessLevel={setAccessLevel}
  81. accessLevel={publicAccessLevel}
  82. inflight={inflight}
  83. />
  84. )
  85. default:
  86. return null
  87. }
  88. }
  89. function PrivateSharing({
  90. setAccessLevel,
  91. inflight,
  92. projectId,
  93. setShowLinks,
  94. }: {
  95. setAccessLevel: (level: AccessLevel) => void
  96. inflight: boolean
  97. projectId: string
  98. setShowLinks: (show: boolean) => void
  99. }) {
  100. const { t } = useTranslation()
  101. return (
  102. <OLRow className="public-access-level">
  103. <OLCol xs={12} className="text-center">
  104. <strong>{t('link_sharing_is_off_short')}</strong>
  105. <span>&nbsp;&nbsp;</span>
  106. <OLButton
  107. variant="link"
  108. className="btn-inline-link"
  109. onClick={() => {
  110. setAccessLevel('tokenBased')
  111. eventTracking.sendMB('link-sharing-click', { projectId })
  112. setShowLinks(true)
  113. }}
  114. disabled={inflight}
  115. >
  116. {t('turn_on_link_sharing')}
  117. </OLButton>
  118. <span>&nbsp;&nbsp;</span>
  119. <LinkSharingInfo />
  120. </OLCol>
  121. </OLRow>
  122. )
  123. }
  124. function TokenBasedSharing({
  125. setAccessLevel,
  126. inflight,
  127. setShowLinks,
  128. showLinks,
  129. }: {
  130. setAccessLevel: (level: AccessLevel) => void
  131. inflight: boolean
  132. setShowLinks: (show: boolean) => void
  133. showLinks: boolean
  134. }) {
  135. const { t } = useTranslation()
  136. const { _id: projectId } = useProjectContext()
  137. const [tokens, setTokens] = useState<Tokens | null>(null)
  138. const { signal } = useAbortController()
  139. useEffect(() => {
  140. getJSON(`/project/${projectId}/tokens`, { signal })
  141. .then(data => setTokens(data))
  142. .catch(debugConsole.error)
  143. }, [projectId, signal])
  144. return (
  145. <OLRow className="public-access-level">
  146. <OLCol xs={12} className="text-center">
  147. <strong>{t('link_sharing_is_on')}</strong>
  148. <span>&nbsp;&nbsp;</span>
  149. <OLButton
  150. variant="link"
  151. className="btn-inline-link"
  152. onClick={() => setAccessLevel('private')}
  153. disabled={inflight}
  154. >
  155. {t('turn_off_link_sharing')}
  156. </OLButton>
  157. <span>&nbsp;&nbsp;</span>
  158. <LinkSharingInfo />
  159. <OLButton
  160. variant="link"
  161. className="btn-chevron align-middle"
  162. onClick={() => setShowLinks(!showLinks)}
  163. >
  164. <MaterialIcon
  165. type={showLinks ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
  166. />
  167. </OLButton>
  168. </OLCol>
  169. {showLinks && (
  170. <OLCol xs={12} className="access-token-display-area">
  171. <div className="access-token-wrapper">
  172. <strong>{t('anyone_with_link_can_edit')}</strong>
  173. <AccessToken
  174. token={tokens?.readAndWrite}
  175. tokenHashPrefix={tokens?.readAndWriteHashPrefix}
  176. path="/"
  177. tooltipId="tooltip-copy-link-rw"
  178. />
  179. </div>
  180. <div className="access-token-wrapper">
  181. <strong>{t('anyone_with_link_can_view')}</strong>
  182. <AccessToken
  183. token={tokens?.readOnly}
  184. tokenHashPrefix={tokens?.readOnlyHashPrefix}
  185. path="/read/"
  186. tooltipId="tooltip-copy-link-ro"
  187. />
  188. </div>
  189. </OLCol>
  190. )}
  191. </OLRow>
  192. )
  193. }
  194. function LegacySharing({
  195. accessLevel,
  196. setAccessLevel,
  197. inflight,
  198. }: {
  199. accessLevel: AccessLevel
  200. setAccessLevel: (level: AccessLevel) => void
  201. inflight: boolean
  202. }) {
  203. const { t } = useTranslation()
  204. return (
  205. <OLRow className="public-access-level">
  206. <OLCol xs={12} className="text-center">
  207. <strong>
  208. {accessLevel === 'readAndWrite' && t('this_project_is_public')}
  209. {accessLevel === 'readOnly' && t('this_project_is_public_read_only')}
  210. </strong>
  211. <span>&nbsp;&nbsp;</span>
  212. <OLButton
  213. variant="link"
  214. className="btn-inline-link"
  215. onClick={() => setAccessLevel('private')}
  216. disabled={inflight}
  217. >
  218. {t('make_private')}
  219. </OLButton>
  220. <span>&nbsp;&nbsp;</span>
  221. <LinkSharingInfo />
  222. </OLCol>
  223. </OLRow>
  224. )
  225. }
  226. export function ReadOnlyTokenLink() {
  227. const { t } = useTranslation()
  228. const { _id: projectId } = useProjectContext()
  229. const [tokens, setTokens] = useState<Tokens | null>(null)
  230. const { signal } = useAbortController()
  231. useEffect(() => {
  232. getJSON(`/project/${projectId}/tokens`, { signal })
  233. .then(data => setTokens(data))
  234. .catch(debugConsole.error)
  235. }, [projectId, signal])
  236. return (
  237. <OLRow className="public-access-level">
  238. <OLCol className="access-token-display-area">
  239. <div className="access-token-wrapper">
  240. <strong>{t('anyone_with_link_can_view')}</strong>
  241. <AccessToken
  242. token={tokens?.readOnly}
  243. tokenHashPrefix={tokens?.readOnlyHashPrefix}
  244. path="/read/"
  245. tooltipId="tooltip-copy-link-ro"
  246. />
  247. </div>
  248. </OLCol>
  249. </OLRow>
  250. )
  251. }
  252. function AccessToken({
  253. token,
  254. tokenHashPrefix,
  255. path,
  256. tooltipId,
  257. }: {
  258. token?: string
  259. tokenHashPrefix?: string
  260. path: string
  261. tooltipId: string
  262. }) {
  263. const { t } = useTranslation()
  264. const { isAdmin } = useUserContext()
  265. if (!token) {
  266. return (
  267. <pre className="access-token">
  268. <span>{t('loading')}…</span>
  269. </pre>
  270. )
  271. }
  272. let origin = window.location.origin
  273. if (isAdmin) {
  274. origin = getMeta('ol-ExposedSettings').siteUrl
  275. }
  276. const link = `${origin}${path}${token}${
  277. tokenHashPrefix ? `#${tokenHashPrefix}` : ''
  278. }`
  279. return (
  280. <div className="access-token">
  281. <code>{link}</code>
  282. <CopyToClipboard content={link} tooltipId={tooltipId} />
  283. </div>
  284. )
  285. }
  286. function LinkSharingInfo() {
  287. const { t } = useTranslation()
  288. return (
  289. <OLTooltip
  290. id="link-sharing-info"
  291. description={t('learn_more_about_link_sharing')}
  292. >
  293. <a
  294. href="/learn/how-to/What_is_Link_Sharing%3F"
  295. target="_blank"
  296. rel="noopener"
  297. >
  298. <MaterialIcon type="help" className="align-middle" />
  299. </a>
  300. </OLTooltip>
  301. )
  302. }