|
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 'telegram' |
|
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 |
|
|