Spaces:
Paused
Paused
import { | |
ArrowLeftOutlined, | |
BugOutlined, | |
CloudDownloadOutlined, | |
CloudUploadOutlined, | |
CrownOutlined, | |
DeleteOutlined, | |
DownloadOutlined, | |
ExpandAltOutlined, | |
ExperimentOutlined, | |
FrownOutlined, | |
InfoOutlined, | |
ImportOutlined, | |
LoginOutlined, | |
LogoutOutlined, | |
MobileOutlined, | |
MonitorOutlined, | |
ReloadOutlined, | |
ExportOutlined, | |
SkinOutlined, | |
SyncOutlined, | |
WarningOutlined | |
} from '@ant-design/icons' | |
import { | |
Avatar, | |
Button, | |
Card, | |
Checkbox, | |
Col, | |
Form, | |
Input, | |
Layout, | |
List, | |
Modal, | |
notification, | |
Popover, | |
Progress, | |
Row, | |
Select, | |
Space, | |
Switch, | |
Tooltip, | |
Typography, | |
Upload | |
} from 'antd' | |
import { useForm } from 'antd/es/form/Form' | |
import prettyBytes from 'pretty-bytes' | |
import pwaInstallHandler from 'pwa-install-handler' | |
import React, { useEffect, useState } from 'react' | |
import { useThemeSwitcher } from 'react-css-theme-switcher' | |
import { useHistory } from 'react-router-dom' | |
import useSWR from 'swr' | |
import { Api } from 'teledrive-client' | |
import * as serviceWorkerRegistration from '../serviceWorkerRegistration' | |
import { VERSION } from '../utils/Constant' | |
import { apiUrl, fetcher, req } from '../utils/Fetcher' | |
import { telegramClient } from '../utils/Telegram' | |
interface Props { | |
me?: any, | |
mutate?: any, | |
error?: any | |
} | |
const Settings: React.FC<Props> = ({ me, mutate, error }) => { | |
const history = useHistory() | |
const [expandableRows, setExpandableRows] = useState<boolean>() | |
const [logoutConfirmation, setLogoutConfirmation] = useState<boolean>(false) | |
const [removeConfirmation, setRemoveConfirmation] = useState<boolean>(false) | |
const [expFeatures, setExpFeatures] = useState<boolean>(false) | |
const [loadingChangeServer, setLoadingChangeServer] = useState<boolean>(false) | |
const [loadingRemove, setLoadingRemove] = useState<boolean>(false) | |
const [destroySession, setDestroySession] = useState<boolean>(false) | |
const [reportBug, setReportBug] = useState<boolean>(false) | |
const [pwa, setPwa] = useState<{ canInstall: boolean, install: () => Promise<boolean> }>() | |
const [dc, setDc] = useState<string>() | |
const [form] = useForm() | |
const [formRemoval] = useForm() | |
const { currentTheme } = useThemeSwitcher() | |
const { data: dialogs } = useSWR('/dialogs?limit=75&offset=0', fetcher) | |
const { data: stats } = useSWR('/files/stats', fetcher) | |
const save = async (settings: any): Promise<void> => { | |
try { | |
await req.patch('/users/me/settings', { settings }) | |
notification.success({ message: 'Saved' }) | |
mutate() | |
} catch ({ response }) { | |
if ((response as any).status === 402) { | |
return notification.error({ | |
message: 'Premium Feature', | |
description: 'Please upgrade your plan for using this feature' | |
}) | |
} | |
return notification.error({ message: 'Something error. Please try again.' }) | |
} | |
} | |
useEffect(() => { | |
if (me) { | |
setExpandableRows(me.user?.settings?.expandable_rows) | |
} | |
}, [me]) | |
useEffect(() => { | |
if (error) { | |
return history.push('/login') | |
} | |
}, [error]) | |
useEffect(() => { | |
pwaInstallHandler.addListener(canInstall => { | |
setPwa({ canInstall, install: pwaInstallHandler.install }) | |
}) | |
}, []) | |
useEffect(() => { | |
if (window.location.host === 'ge.teledriveapp.com') { | |
setDc('ge') | |
localStorage.setItem('dc', 'ge') | |
} else if (window.location.host === 'us.teledriveapp.com') { | |
setDc('us') | |
localStorage.setItem('dc', 'us') | |
} else { | |
setDc('sg') | |
localStorage.setItem('dc', 'sg') | |
} | |
}, []) | |
useEffect(() => { | |
if (dc) { | |
form.setFieldsValue({ change_server: dc }) | |
} | |
}, [dc]) | |
useEffect(() => { | |
if (me) { | |
form.setFieldsValue({ saved_location: me?.user.settings?.saved_location || 'me' }) | |
} | |
}, [me]) | |
const logout = async () => { | |
await req.post('/auth/logout', {}, destroySession ? { params: { destroySession: 1 } } : undefined) | |
window.localStorage.removeItem('experimental') | |
return window.location.replace('/') | |
} | |
const remove = async () => { | |
setLoadingRemove(true) | |
const { agreement, reason } = formRemoval.getFieldsValue() | |
try { | |
await req.post('/users/me/delete', { agreement, reason }) | |
setRemoveConfirmation(false) | |
setLoadingRemove(false) | |
return window.location.replace('/') | |
} catch (error: any) { | |
setLoadingRemove(false) | |
return notification.error({ message: 'Error', description: <> | |
<Typography.Paragraph> | |
{error?.response?.data?.error || error.message || 'Something error'} | |
</Typography.Paragraph> | |
<Typography.Paragraph code> | |
{JSON.stringify(error?.response?.data || error?.data || error, null, 2)} | |
</Typography.Paragraph> | |
</> }) | |
} | |
} | |
const exportFilesData = async () => { | |
setLoadingChangeServer(true) | |
const { data } = await req.get('/files', { params: { | |
full_properties: 1, | |
sort: 'created_at', | |
offset: 0, | |
limit: 10 | |
} }) | |
const files = data?.files || [] | |
while (files?.length < data.length) { | |
const { data } = await req.get('/files', { params: { | |
full_properties: 1, | |
sort: 'created_at', | |
offset: 0, | |
limit: 10 | |
} }) | |
files.push(...data.files) | |
} | |
const hiddenElement = document.createElement('a') | |
hiddenElement.href = 'data:attachment/text,' + encodeURI(JSON.stringify(files) || '') | |
hiddenElement.target = '_blank' | |
hiddenElement.download = 'files.json' | |
hiddenElement.click() | |
setLoadingChangeServer(false) | |
} | |
const downloadLogs = async () => { | |
const hiddenElement = document.createElement('a') | |
hiddenElement.href = 'data:attachment/text,' + encodeURI(sessionStorage.getItem('requests') || '') | |
hiddenElement.target = '_blank' | |
hiddenElement.download = 'logs.json' | |
hiddenElement.click() | |
} | |
const emailLink = () => `mailto:bug@teledriveapp.com?subject=TeleDrive%20-%20Bug%20Report&body=User%3A%20${decodeURIComponent(me?.user.username)}%0D%0AOrigin%3A%20${decodeURIComponent(window.location.origin)}%0D%0ADevice%3A%20${decodeURIComponent(navigator.userAgent)}%0D%0AProblem%3A%20%3CPlease%20describe%20your%20problem%20here%3E%0D%0AExpectation%3A%20%3CPlease%20describe%20your%20expectation%20here%3E` | |
const buildPathDialog = (dialog: any) => { | |
const peerType = dialog.isUser ? 'user' : dialog.isChannel ? 'channel' : 'chat' | |
return `${peerType}/${dialog.entity?.id}/_${dialog.entity?.accessHash ? `/${dialog.entity?.accessHash}` : ''}` | |
} | |
return <> | |
<Layout.Content> | |
<Row style={{ margin: '50px 12px 100px' }}> | |
<Col xxl={{ span: 8, offset: 8 }} xl={{ span: 10, offset: 7 }} lg={{ span: 12, offset: 6 }} md={{ span: 14, offset: 5 }} span={24}> | |
<Typography.Title> | |
Settings | |
</Typography.Title> | |
<Card loading={!me && !error} title={<Card.Meta avatar={<Avatar size="large" src={`${apiUrl}/users/me/photo`} />} title={<>{me?.user.name} {me?.user?.plan === 'premium' && <Popover placement="top" content={<Layout style={{ padding: '7px 13px' }}>Premium</Layout>}> | |
<CrownOutlined /> | |
</Popover>}</>} description={me?.user.username} />} actions={[<Row style={{ marginTop: '15px' }}> | |
<Col span={22} offset={1} md={{ span: 12, offset: 6 }}> | |
<Typography.Paragraph style={{ textAlign: 'center' }}> | |
<Button block icon={<LogoutOutlined />} danger shape="round" | |
onClick={() => setLogoutConfirmation(true)}> | |
Logout | |
</Button> | |
</Typography.Paragraph> | |
<Typography.Paragraph style={{ textAlign: 'center' }}> | |
<Button block icon={<ArrowLeftOutlined />} type="link" | |
onClick={() => history.push('/dashboard')}> | |
Back to Dashboard | |
</Button> | |
</Typography.Paragraph> | |
<Typography.Paragraph style={{ textAlign: 'center' }} type="secondary"> | |
v{VERSION} | |
</Typography.Paragraph> | |
</Col> | |
</Row>]}> | |
<Form form={form} layout="horizontal" labelAlign="left" labelCol={{ span: 12 }} wrapperCol={{ span: 12 }}> | |
{stats?.stats && <List header="Stats Info" bordered={false}> | |
<List.Item key="fileTotalSize"> | |
<List.Item.Meta title="Uploaded Files" description={<Space direction="horizontal" align="center" style={{ marginTop: '13px' }}> | |
<Progress width={150} type="circle" status="active" format={() => <> | |
<Typography.Title level={3}>{prettyBytes(Number(stats.stats.totalUserFilesSize))}</Typography.Title> | |
<Typography.Paragraph style={{ fontSize: '12px' }} type="secondary">User Files Size</Typography.Paragraph> | |
</>} percent={Number((Number(stats.stats.totalUserFilesSize) / Number(stats.stats.totalFilesSize) * 100).toFixed(1))} /> | |
<Progress width={150} type="circle" status="success" format={() => <> | |
<Typography.Title level={3}>{prettyBytes(Number(stats.stats.totalFilesSize))}</Typography.Title> | |
<Typography.Paragraph style={{ fontSize: '12px' }} type="secondary">Total Files Size</Typography.Paragraph> | |
</>} percent={100} /> | |
</Space>} /> | |
</List.Item> | |
<List.Item key="system"> | |
<List.Item.Meta title="System Disk Usage" description={<Tooltip title={`Available ${prettyBytes(stats.stats.system.free)}/${prettyBytes(stats.stats.system.size)}`}> | |
<Progress status="active" percent={Number((stats.stats.system.free / stats.stats.system.size * 100).toFixed(1))} /> | |
</Tooltip>} /> | |
</List.Item> | |
<List.Item key="cached"> | |
<List.Item.Meta title="Cached Total Size" description={<Tooltip title={prettyBytes(stats.stats.cachedSize)}> | |
<Progress status="active" percent={Number((stats.stats.cachedSize / stats.stats.system.size * 100).toFixed(1))} /> | |
</Tooltip>} /> | |
</List.Item> | |
</List>} | |
<List header="Interface" bordered={false}> | |
{pwa?.canInstall && <List.Item key="install" actions={[<Form.Item> | |
<Button shape="round" icon={<MobileOutlined />} onClick={pwa?.install}>Install</Button> | |
</Form.Item>]}> | |
<List.Item.Meta title={<Space><DownloadOutlined /><>Install App</></Space>} description="Install TeleDrive to your device" /> | |
</List.Item>} | |
<List.Item key="expandable-rows" actions={[<Form.Item name="expandable_rows"> | |
<Switch onChange={val => { | |
setExpandableRows(val) | |
save({ expandable_rows: val }) | |
}} checked={expandableRows} defaultChecked={expandableRows} /> | |
</Form.Item>]}> | |
<List.Item.Meta title={<Space><ExpandAltOutlined /><>Expandable Rows</></Space>} description="Show file details in row table" /> | |
</List.Item> | |
<List.Item key="dark-mode" actions={[<Form.Item name="dark_mode"> | |
<Switch onChange={(val: boolean) => save({ theme: val ? 'dark' : 'light' }).then(window.location.reload)} checked={currentTheme === 'dark'} defaultChecked={currentTheme === 'dark'} /> | |
</Form.Item>]}> | |
<List.Item.Meta title={<Space><SkinOutlined /><>Dark Mode</></Space>} description="Join the dark side" /> | |
</List.Item> | |
</List> | |
<List header="Operational"> | |
{dialogs?.dialogs && <List.Item key="saved-location" actions={[<Form.Item name="saved_location"> | |
<Select className="saved-location ghost" showSearch | |
filterOption={(input, option: any) => !option.children.toLowerCase().indexOf(input.toLowerCase())} | |
onChange={saved_location => save({ saved_location: saved_location === 'me' ? null : saved_location })}> | |
<Select.Option key="me" value="me">Saved Messages</Select.Option> | |
{dialogs?.dialogs.filter((d: any) => d.entity.id != me?.user.tg_id).map((dialog: any) => <Select.Option key={dialog.entity.id} value={buildPathDialog(dialog)}>{dialog.title}</Select.Option>)} | |
</Select> | |
</Form.Item>]}> | |
<List.Item.Meta title={<Space><CloudUploadOutlined /><>Upload Destination</></Space>} description="Select where to save files" /> | |
</List.Item>} | |
<List.Item key="check-for-updates" actions={[<Form.Item> | |
<Button shape="round" icon={<ReloadOutlined />} onClick={() => { | |
serviceWorkerRegistration.unregister(); | |
(window.location as any).reload(true) | |
}}>Reload</Button> | |
</Form.Item>]}> | |
<List.Item.Meta title={<Space><SyncOutlined /><>Check Updates</></Space>} description="Reload to checking for updates" /> | |
</List.Item> | |
<List.Item key="report-bugs" actions={[<Form.Item> | |
<Button shape="round" icon={<BugOutlined />} onClick={() => window.open('https://github.com/mgilangjanuar/teledrive/issues/new?assignees=&labels=bug&template=bug_report.md&title=', '_blank')}>Report</Button> | |
</Form.Item>]}> | |
<List.Item.Meta title={<Space><MonitorOutlined /><>Report Bug</></Space>} description="Send your activities for reporting" /> | |
</List.Item> | |
</List> | |
<List header="Data"> | |
<List.Item key="export" actions={[<Form.Item> | |
<Button shape="round" loading={loadingChangeServer} icon={<CloudDownloadOutlined />} onClick={exportFilesData}>Export</Button> | |
</Form.Item>]}> | |
<List.Item.Meta title={<Space><ExportOutlined /><>Save Data</></Space>} description="Export your files ref data as JSON" /> | |
</List.Item> | |
<List.Item key="import" actions={[<Form.Item> | |
<Button shape="round" icon={<CloudUploadOutlined />}> | |
<Upload name="upload" fileList={[]} multiple={false} beforeUpload={file => { | |
const fileReader = new FileReader() | |
fileReader.readAsText(file, 'UTF-8') | |
fileReader.onload = async ({ target }) => { | |
await req.post('/files/filesSync', { files: JSON.parse(target?.result as string || '[]') }) | |
notification.success({ | |
message: 'Import Successfully', | |
description: 'Your files has been imported successfully but you need to reshare your files again to update your shared files', | |
}) | |
} | |
}}>Import</Upload> | |
</Button> | |
</Form.Item>]}> | |
<List.Item.Meta title={<Space><ImportOutlined /><>Import Data</></Space>} description="Import your files ref data" /> | |
</List.Item> | |
</List> | |
<List header="Danger Zone"> | |
<List.Item key="join-exp" actions={[<Form.Item> | |
<Button shape="round" icon={localStorage.getItem('experimental') && localStorage.getItem('session') ? <LogoutOutlined /> : <LoginOutlined />} onClick={async () => { | |
if (localStorage.getItem('experimental') && localStorage.getItem('session')) { | |
const client = await telegramClient.connect() | |
localStorage.removeItem('experimental') | |
localStorage.removeItem('session') | |
location.reload() | |
try { | |
await client.invoke(new Api.auth.LogOut()) | |
} catch (error) { | |
// ignore | |
} | |
} else { | |
setExpFeatures(true) | |
} | |
}}>{localStorage.getItem('experimental') && localStorage.getItem('session') ? 'Revoke' : 'Join'}</Button> | |
</Form.Item>]}> | |
<List.Item.Meta title={<Space><ExperimentOutlined /><>Experimental</></Space>} description="Join to the experimental features" /> | |
</List.Item> | |
<List.Item key="delete-account" actions={[<Form.Item> | |
<Button shape="round" danger type="primary" icon={<FrownOutlined />} onClick={() => setRemoveConfirmation(true)}>Delete</Button> | |
</Form.Item>]}> | |
<List.Item.Meta title={<Typography.Text type="danger"><Space><DeleteOutlined /><>Delete Account</></Space></Typography.Text>} description="Delete your account permanently" /> | |
</List.Item> | |
</List> | |
</Form> | |
</Card> | |
</Col> | |
</Row> | |
</Layout.Content> | |
<Modal title={<Typography.Text> | |
<Typography.Text type="warning"><WarningOutlined /></Typography.Text> Logout Confirmation | |
</Typography.Text>} | |
visible={logoutConfirmation} | |
onCancel={() => setLogoutConfirmation(false)} | |
onOk={logout} | |
cancelButtonProps={{ shape: 'round' }} | |
okButtonProps={{ danger: true, type: 'primary', shape: 'round' }}> | |
<Typography.Paragraph> | |
Are you sure to logout? | |
</Typography.Paragraph> | |
<Form.Item help="All files you share will not be able to download once you sign out"> | |
<Checkbox checked={destroySession} onChange={({ target }) => setDestroySession(target.checked)}> | |
Also delete my active session | |
</Checkbox> | |
</Form.Item> | |
</Modal> | |
<Modal title={<Typography.Text> | |
<Typography.Text type="warning"><WarningOutlined /></Typography.Text> This action cannot be undone | |
</Typography.Text>} | |
visible={removeConfirmation} | |
onCancel={() => setRemoveConfirmation(false)} | |
onOk={formRemoval.submit} | |
cancelButtonProps={{ shape: 'round' }} | |
okButtonProps={{ danger: true, type: 'primary', shape: 'round', loading: loadingRemove }}> | |
<Form form={formRemoval} onFinish={remove} layout="vertical"> | |
<Form.Item name="reason" label="Reason" rules={[{ required: true, message: 'Please input your reason' }]}> | |
<Input.TextArea /> | |
</Form.Item> | |
<Form.Item name="agreement" label={<Typography.Text>Please type <Typography.Text type="danger">permanently removed</Typography.Text> for your confirmation</Typography.Text>} rules={[{ required: true, message: 'Please input the confirmation' }]}> | |
<Input /> | |
</Form.Item> | |
</Form> | |
</Modal> | |
{/* <Modal title={<Typography.Text> | |
<Typography.Text type="warning"><WarningOutlined /></Typography.Text> Change Server Confirmation | |
</Typography.Text>} | |
visible={!!changeDCConfirmation} | |
onCancel={() => setChangeDCConfirmation(undefined)} | |
onOk={changeServer} | |
cancelButtonProps={{ shape: 'round' }} | |
okButtonProps={{ danger: true, type: 'primary', shape: 'round', loading: loadingChangeServer }}> | |
<Typography.Paragraph> | |
Are you sure to change the server region to {changeDCConfirmation === 'ge' ? 'Frankfurt' : changeDCConfirmation === 'us' ? 'New York' : 'Singapore'}? | |
</Typography.Paragraph> | |
<Typography.Paragraph type="secondary"> | |
You'll be logged out and redirected to the new server. Please login again to that new server. | |
</Typography.Paragraph> | |
</Modal> */} | |
<Modal title={<Typography.Text> | |
<Typography.Text><InfoOutlined /></Typography.Text> Report Bugs | |
</Typography.Text>} | |
visible={reportBug} | |
onCancel={() => setReportBug(false)} | |
onOk={undefined} | |
okText="Send Email" | |
cancelButtonProps={{ shape: 'round' }} | |
okButtonProps={{ type: 'primary', shape: 'round', href: emailLink() }}> | |
<Typography.Paragraph> | |
Please follow these instructions: | |
</Typography.Paragraph> | |
<ol> | |
<li> | |
Download <a onClick={downloadLogs}>your logs</a> | |
</li> | |
<li> | |
Send an email to <a href={emailLink()}>bug@teledriveapp.com</a> with logs and additional screenshots in the attachment | |
</li> | |
</ol> | |
</Modal> | |
<Modal title={<Typography.Text> | |
<Typography.Text type="warning"><WarningOutlined /></Typography.Text> Join Experimental | |
</Typography.Text>} | |
visible={expFeatures} | |
onCancel={() => { | |
localStorage.removeItem('experimental') | |
setExpFeatures(false) | |
}} | |
onOk={() => { | |
localStorage.setItem('experimental', 'true') | |
setExpFeatures(false) | |
window.open(`${window.location.origin}/login`, '_blank', 'location=yes,height=720,width=520,scrollbars=yes,status=yes,top=100,left=300') | |
}} | |
cancelButtonProps={{ shape: 'round' }} | |
okButtonProps={{ type: 'primary', shape: 'round' }}> | |
<Typography.Paragraph> | |
You will get this experimental features: | |
</Typography.Paragraph> | |
<ul> | |
<li> | |
<strong>Ultra Upload</strong> | |
<Typography.Paragraph> | |
Your files will directly upload to the Telegram servers and the speed will follow your internet connection. | |
</Typography.Paragraph> | |
</li> | |
<li> | |
<strong>Fast Download</strong> | |
<Typography.Paragraph> | |
Same like Ultra Upload, your files will be downloaded directly from the Telegram servers. But, it will have some limitations: | |
<ul> | |
<li>Only works with chrome-based browsers</li> | |
<li>The max download size is 2GB for free users or follows your device memory</li> | |
</ul> | |
</Typography.Paragraph> | |
</li> | |
</ul> | |
<Typography.Paragraph> | |
Note. Those features may have bugs please report them to <a href={emailLink()}>bug@teledriveapp.com</a> and you can always revoke from experimental features anytime. | |
</Typography.Paragraph> | |
<Typography.Paragraph strong> | |
You need to be logged in again to TeleDrive. Continue? | |
</Typography.Paragraph> | |
</Modal> | |
</> | |
} | |
export default Settings | |