import React, { Component } from 'react'; import { API_BASE, SafeTextarea, PromotionBar, HighlightedMarkdown, test_ipfs, } from './Common'; import { MessageViewer } from './Message'; import { LoginPopup } from './infrastructure/widgets'; import { ColorPicker } from './color_picker'; import { ConfigUI } from './Config'; import copy from 'copy-to-clipboard'; import { cache } from './cache'; import { API, get_json } from './flows_api'; import { save_attentions } from './Attention'; import './UserAction.css'; const REPOSITORY = 'https://git.thu.monster/newthuhole/'; const EMAIL = 'hole_thu@riseup.net'; export const TokenCtx = React.createContext({ value: null, set_value: () => {}, }); export function InfoSidebar(props) { return (
   { props.show_sidebar('设置', ); }} >   

强烈建议开始使用前先看一遍所有设置选项

{ if ('serviceWorker' in navigator) { navigator.serviceWorker .getRegistrations() .then((registrations) => { for (let registration of registrations) { console.log('unregister', registration); registration.unregister(); } }); } cache().clear(); setTimeout(() => { window.location.reload(true); }, 200); }} > 强制检查更新 (当前版本:【{process.env.REACT_APP_BUILD_INFO || '---'}{' '} {process.env.NODE_ENV}】 会自动在后台检查更新并在下次访问时更新)

意见反馈请加tag #意见反馈 或到github后端的issue区。

新T树洞强烈期待有其他更多树洞的出现,一起分布式互联,构建清华树洞族。详情见 关于 中的描述。

联系我们:{EMAIL}

新T树洞 网页版 by @hole_thu,基于 AGPLv3 协议在{' '} Gitea {' '} 开源。

新T树洞 网页版基于 P大树洞网页版 by @xmcp T大树洞网页版 by @thuhole 、{' '} React IcoMoon 等开源项目。


新T树洞 后端 by @hole_thu,基于 WTFPLv2 协议在{' '} Gitea {' '} 开源。

); } export class LoginForm extends Component { constructor(props) { super(props); this.state = { custom_title: window.TITLE || '', }; } update_title(title, token) { if (title === window.TITLE) { alert('无变化'); return; } API.set_title(title, token) .then((json) => { if (json.code === 0) { window.TITLE = title; alert('专属头衔设置成功'); } }) .catch((err) => alert('设置头衔出错了:\n' + err)); } copy_token(token) { if (copy(token)) alert('复制成功!\n请一定不要泄露哦'); } render() { return ( {(token) => (
{/*{!!token.value &&*/} {/* */} {/*}*/}
{token.value ? (

您已登录。

{ this.props.show_sidebar( '系统日志', , ); }} > 查看系统日志
举报记录、管理日志等都是公开的。

复制 User Token
User Token仅用于开发bot,切勿告知他人。若怀疑被盗号请刷新Token(刷新功能即将上线)。

专属头衔: { this.setState({ custom_title: e.target.value }); }} maxLength={10} />
设置专属头衔后,可在发言时选择使用。重置后需重新设置。临时用户如需保持头衔请使用相同后缀。

) : ( {(do_popup) => (

新T树洞 面向T大学生,通过已验证身份的第三方服务授权登陆。

)}
)}
)}
); } } export class ReplyForm extends Component { constructor(props) { super(props); this.state = { text: '', loading_status: 'done', preview: false, use_title: false, }; this.on_change_bound = this.on_change.bind(this); this.on_use_title_change_bound = this.on_use_title_change.bind(this); this.area_ref = this.props.area_ref || React.createRef(); this.global_keypress_handler_bound = this.global_keypress_handler.bind( this, ); this.color_picker = new ColorPicker(); } global_keypress_handler(e) { if ( e.code === 'Enter' && !e.ctrlKey && !e.altKey && ['input', 'textarea'].indexOf(e.target.tagName.toLowerCase()) === -1 ) { if (this.area_ref.current) { e.preventDefault(); this.area_ref.current.focus(); } } } componentDidMount() { document.addEventListener('keypress', this.global_keypress_handler_bound); } componentWillUnmount() { document.removeEventListener( 'keypress', this.global_keypress_handler_bound, ); } on_change(value) { this.setState({ text: value, }); } on_use_title_change(event) { this.setState({ use_title: event.target.checked, }); } on_submit(event) { if (event) event.preventDefault(); if (this.state.loading_status === 'loading') return; if (!this.state.text) return; this.setState({ loading_status: 'loading', }); const { pid } = this.props; const { text, use_title } = this.state; let data = new URLSearchParams({ pid: pid, text: text, use_title: use_title ? '1' : '', }); fetch(API_BASE + '/docomment', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Token': this.props.token, }, body: data, }) .then(get_json) .then((json) => { if (json.code !== 0) { throw new Error(json.msg); } let saved_attentions = window.saved_attentions; if (!saved_attentions.includes(pid)) { saved_attentions.unshift(pid); window.saved_attentions = saved_attentions; save_attentions(); } this.setState({ loading_status: 'done', text: '', preview: false, }); this.area_ref.current.clear(); this.props.on_complete(); }) .catch((e) => { console.error(e); alert('回复失败\n' + e); this.setState({ loading_status: 'done', }); }); } toggle_preview() { this.setState({ preview: !this.state.preview, }); } render() { return (
{this.state.preview ? (
{}} />
) : ( )}
{this.state.loading_status === 'loading' ? ( ) : ( )}
{window.TITLE && ( )}
); } } export class PostForm extends Component { constructor(props) { super(props); this.state = { text: '', upload_progress_text: '', is_loading: false, file_name: '', file_type: '', cw: window.CW_BACKUP || '', allow_search: window.AS_BACKUP || false, loading_status: 'done', preview: false, has_poll: !!window.POLL_BACKUP, poll_options: JSON.parse(window.POLL_BACKUP || '[""]'), use_title: false, }; this.area_ref = React.createRef(); this.on_change_bound = this.on_change.bind(this); this.on_allow_search_change_bound = this.on_allow_search_change.bind(this); this.on_use_title_change_bound = this.on_use_title_change.bind(this); this.on_cw_change_bound = this.on_cw_change.bind(this); this.on_poll_option_change_bound = this.on_poll_option_change.bind(this); this.color_picker = new ColorPicker(); } componentDidMount() { if (this.area_ref.current) this.area_ref.current.focus(); } componentWillUnmount() { const { cw, allow_search, has_poll, poll_options } = this.state; window.CW_BACKUP = cw; window.AS_BACKUP = allow_search; localStorage['DEFAULT_ALLOW_SEARCH'] = allow_search ? '1' : ''; window.POLL_BACKUP = has_poll ? JSON.stringify(poll_options) : null; } on_allow_search_change(event) { this.setState({ allow_search: event.target.checked, }); } on_use_title_change(event) { this.setState({ use_title: event.target.checked, }); } on_cw_change(event) { this.setState({ cw: event.target.value, }); } on_change(value) { this.setState({ text: value, }); } do_post() { const { cw, text, allow_search, use_title, has_poll, poll_options, } = this.state; let data = new URLSearchParams({ cw: cw, text: text, allow_search: allow_search ? '1' : '', use_title: use_title ? '1' : '', type: 'text', }); if (has_poll) { poll_options.forEach((opt) => { if (opt) data.append('poll_options', opt); }); } fetch(API_BASE + '/dopost', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Token': this.props.token, }, body: data, }) .then(get_json) .then((json) => { if (json.code !== 0) { throw new Error(json.msg); } this.setState({ loading_status: 'done', text: '', preview: false, }); this.area_ref.current.clear(); this.props.on_complete(); window.CW_BACKUP = ''; window.POLL_BACKUP = null; }) .catch((e) => { console.error(e); alert('发表失败\n' + e); this.setState({ loading_status: 'done', }); }); } on_submit(event) { if (event) event.preventDefault(); if (this.state.loading_status === 'loading') return; if (!this.state.text) return; { this.setState({ loading_status: 'loading', }); this.do_post(); } } toggle_preview() { this.setState({ preview: !this.state.preview, }); } on_poll_option_change(event, idx) { let poll_options = this.state.poll_options; let text = event.target.value; poll_options[idx] = text; if (!text && poll_options.length > 1) { poll_options.splice(idx, 1); } if (poll_options[poll_options.length - 1] && poll_options.length < 8) { poll_options.push(''); } this.setState({ poll_options: poll_options }); } on_file_change(event) { console.log(event); let f = event.target.files[0]; if (f) { console.log(f); this.setState({ is_loading: true, file_name: f.name, file_type: f.type }); // let data = new FormData(); // data.append('file', f); var xh = new XMLHttpRequest(); xh.upload.addEventListener( 'progress', this.upload_progress.bind(this), false, ); xh.addEventListener('load', this.upload_complete.bind(this), false); xh.addEventListener('error', this.upload_error.bind(this), false); xh.addEventListener('abort', this.upload_abort.bind(this), false); xh.open('POST', API_BASE + '/upload'); xh.setRequestHeader('User-Token', this.props.token); xh.send(f); } } update_text_after_upload(data) { const { file_name, file_type } = this.state; let url = (window.config.ipfs_gateway_list[0] || '(无ipfs网关)').replaceAll( '', data.hash, ) + `?filename=${encodeURIComponent(file_name)}&filetype=${encodeURIComponent( file_type, )}`; test_ipfs(data.hash); let new_text = this.state.text + '\n' + (file_type.startsWith('image/') ? `![](${url})` : url); this.setState({ text: new_text }); this.area_ref.current.set(new_text); } upload_progress(event) { console.log(event.loaded, event.total); this.setState({ upload_progress_text: `${((event.loaded * 100) / event.total).toFixed( 2, )}%`, }); } upload_complete(event) { try { let j = JSON.parse(event.target.responseText); if (j.code != 0) { alert(j.msg); throw new Error(); } this.update_text_after_upload(j.data); this.setState({ is_loading: false }); } catch (e) { console.log(e); this.upload_error(event); } } upload_error(event) { alert( '上传失败\n' + (event.target.responseText.length < 100 ? event.target.responseText : event.target.status), ); this.setState({ is_loading: false }); } upload_abort(event) { alert('上传已中断'); this.setState({ is_loading: false }); } render() { const { has_poll, poll_options, preview, loading_status, upload_progress_text, is_loading, file_name, } = this.state; return (
{preview ? ( ) : ( )} {loading_status !== 'done' ? ( ) : ( )}
{window.TITLE && ( )}
{preview ? (
{}} />
) : ( <> 上传并插入文件: {is_loading && (

上传 {file_name} 中: {upload_progress_text}

)} )} {has_poll && (
投票选项
{poll_options.map((option, idx) => ( this.on_poll_option_change_bound(e, idx)} maxLength="32" /> ))}
)}


请遵守 树洞管理规范(试行) ,文明发言。

首选ipfs网关可以在设置中修改,如效果不佳仍可使用图床,例如: 路过图床 sm.ms 未名BBS 知乎

); } }