import React, { Component } from 'react';
import {
API_BASE,
SafeTextarea,
PromotionBar,
HighlightedMarkdown,
} from './Common';
import { MessageViewer } from './Message';
import { LoginPopup } from './infrastructure/widgets';
import { ColorPicker } from './color_picker';
import { ConfigUI } from './Config';
import fixOrientation from 'fix-orientation';
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 BASE64_RATE = 4 / 3;
const MAX_IMG_DIAM = 8000;
const MAX_IMG_PX = 5000000;
const MAX_IMG_FILESIZE = 450000 * BASE64_RATE;
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 (
意见反馈请加tag #意见反馈 或到github后端的issue区。
新T树洞强烈期待有其他更多树洞的出现,一起分布式互联,构建清华树洞族。详情见 关于 中的描述。
联系我们:{EMAIL} 。
);
}
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 (
);
}
}
export class PostForm extends Component {
constructor(props) {
super(props);
this.state = {
text: '',
cw: window.CW_BACKUP || '',
allow_search: window.AS_BACKUP || false,
loading_status: 'done',
img_tip: null,
preview: false,
has_poll: !!window.POLL_BACKUP,
poll_options: JSON.parse(window.POLL_BACKUP || '[""]'),
use_title: false,
};
this.img_ref = React.createRef();
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.on_img_change_bound = this.on_img_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',
});
});
}
proc_img(file) {
return new Promise((resolve, reject) => {
function return_url(url) {
const idx = url.indexOf(';base64,');
if (idx === -1) throw new Error('img not base64 encoded');
return url.substr(idx + 8);
}
let reader = new FileReader();
function on_got_img(url) {
const image = new Image();
image.onload = () => {
let width = image.width;
let height = image.height;
let compressed = false;
if (width > MAX_IMG_DIAM) {
height = (height * MAX_IMG_DIAM) / width;
width = MAX_IMG_DIAM;
compressed = true;
}
if (height > MAX_IMG_DIAM) {
width = (width * MAX_IMG_DIAM) / height;
height = MAX_IMG_DIAM;
compressed = true;
}
if (height * width > MAX_IMG_PX) {
let rate = Math.sqrt((height * width) / MAX_IMG_PX);
height /= rate;
width /= rate;
compressed = true;
}
console.log('chosen img size', width, height);
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
ctx.drawImage(image, 0, 0, width, height);
let quality_l = 0.1,
quality_r = 0.9,
quality,
new_url;
while (quality_r - quality_l >= 0.03) {
quality = (quality_r + quality_l) / 2;
new_url = canvas.toDataURL('image/jpeg', quality);
console.log(
quality_l,
quality_r,
'trying quality',
quality,
'size',
new_url.length,
);
if (new_url.length <= MAX_IMG_FILESIZE) quality_l = quality;
else quality_r = quality;
}
if (quality_l >= 0.101) {
console.log('chosen img quality', quality);
resolve({
img: return_url(new_url),
quality: quality,
width: Math.round(width),
height: Math.round(height),
compressed: compressed,
});
} else {
reject('图片过大,无法上传');
}
};
image.src = url;
}
reader.onload = (event) => {
fixOrientation(event.target.result, {}, (fixed_dataurl) => {
on_got_img(fixed_dataurl);
});
};
reader.readAsDataURL(file);
});
}
on_img_change() {
if (this.img_ref.current && this.img_ref.current.files.length)
this.setState(
{
img_tip: '(正在处理图片……)',
},
() => {
this.proc_img(this.img_ref.current.files[0])
.then((d) => {
this.setState({
img_tip:
`(${d.compressed ? '压缩到' : '尺寸'} ${d.width}*${
d.height
} / ` +
`质量 ${Math.floor(d.quality * 100)}% / ${Math.floor(
d.img.length / BASE64_RATE / 1000,
)}KB)`,
});
})
.catch((e) => {
this.setState({
img_tip: `图片无效:${e}`,
});
});
},
);
else
this.setState({
img_tip: null,
});
}
on_submit(event) {
if (event) event.preventDefault();
if (this.state.loading_status === 'loading') return;
if (!this.state.text) return;
/*
if (this.img_ref.current.files.length) {
this.setState({
loading_status: 'processing',
});
this.proc_img(this.img_ref.current.files[0])
.then((d) => {
this.setState({
loading_status: 'loading',
});
this.do_post(this.state.text, d.img);
})
.catch((e) => {
alert(e);
});
} else */
{
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 });
}
render() {
const { has_poll, poll_options, preview, loading_status } = this.state;
return (
);
}
}