Skip to content

Commit

Permalink
Merge pull request #33 from qnkhuat/chat_tui
Browse files Browse the repository at this point in the history
Chat tui
  • Loading branch information
qnkhuat authored Jul 14, 2021
2 parents f972648 + a491c21 commit 74f1003
Show file tree
Hide file tree
Showing 23 changed files with 1,049 additions and 420 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# TStream - Streaming from terminal

🙋‍♂️ Come stream at [tstream.club](https://tstream.club)

![TStream](./client/public/demo.gif)


# How to start streaming
Please refer to this [link](https://tstream.club/start-streaming)

# RoadMap
# Upcoming features
- [x] One command to stream terminal session to web => just like tty-share
- [x] In room Chat
- [ ] Voice chat
- [ ] Stream playback
- [ ] User management system
- [ ] User management system
Binary file added client/public/chat.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
250 changes: 144 additions & 106 deletions client/src/components/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import PubSub from "../lib/pubsub";
import TextField from '@material-ui/core/TextField';
import KeyboardArrowRightRoundedIcon from '@material-ui/icons/KeyboardArrowRightRounded';

// key in local storage
const USER_CONFIG_KEY = "tstreamUser";

interface TstreamUser {
name: string,
color: string,
Expand All @@ -14,7 +17,7 @@ interface Props {
className?: string;
}

interface ChatMsg {
export interface ChatMsg {
Name: string;
Content: string;
Color: string;
Expand All @@ -24,10 +27,9 @@ interface ChatMsg {
interface State {
msgList: ChatMsg[];
inputContent: string;
name: string;
color: string;
userConfig: TstreamUser | null;
isWaitingUsername: boolean,
tempMsg: string,
tempMsg: string,
}

const ChatSection: React.FC<ChatMsg> = ({ Name, Content, Color, Time}) => {
Expand All @@ -37,19 +39,14 @@ const ChatSection: React.FC<ChatMsg> = ({ Name, Content, Color, Time}) => {
<div className="break-all">
{
Name === '' ?
<div className="font-bold">
{
Content === "Invalid Username" ?
<img src="./warning.png" alt="warning" height="30" width="30" className="inline-block m-2"/> :
<img src="./hand-wave.png" alt="hand-wave" />
}
<div className="font-bold">
<p>{Content}</p>
</div> :
<>
<span style={{color: Color}} className="font-black">{Name}</span>
<span className="text-green-600 py-1"><KeyboardArrowRightRoundedIcon /></span>
{Content}
</div> :
<>
<span style={{color: Color}} className="font-black">{Name}</span>
<span className="text-green-600 py-1"><KeyboardArrowRightRoundedIcon /></span>
{Content}
</>
</>
}
</div>
</div>
Expand All @@ -61,11 +58,12 @@ class Chat extends React.Component<Props, State> {

constructor(props: Props) {
super(props);

let userConfig = this.getUserConfig()
this.state = {
msgList: [],
inputContent: '',
name: '',
color: '',
userConfig: userConfig,
isWaitingUsername: false,
tempMsg: '',
}
Expand All @@ -81,7 +79,7 @@ class Chat extends React.Component<Props, State> {
}

componentDidMount() {
this.props.msgManager?.sub(constants.MSG_TCHAT, (cacheChat: Array<ChatMsg>) => {
this.props.msgManager?.sub(constants.MSG_TCHAT_IN, (cacheChat: Array<ChatMsg>) => {
if (cacheChat === null) {
return;
}
Expand All @@ -94,110 +92,152 @@ class Chat extends React.Component<Props, State> {
});
});

const payload = localStorage.getItem('tstreamUser');
if (payload !== null) {
const tstreamUser : TstreamUser = JSON.parse(payload);
this.setState({
name: tstreamUser.name,
color: tstreamUser.color,
});
}

// disable enter default behavior of textarea
document.getElementById("textarea")!.addEventListener('keydown', (e) => {
var code = e.keyCode || e.which;
if (code === 13) {
document.getElementById("chat-input")!.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.onSendMsg(this.state.inputContent);

let content = this.state.inputContent;
if (content.length > 0 && content[0] == "/") {
this.handleCommand(content.slice(1));
} else {
this.handleSendMsg(content);
}
this.setState({
inputContent: "",
});
}
});

this.props.msgManager.pub("request", constants.MSG_TREQUEST_CACHE_CHAT);
}

// command doesn't include the first '/'
handleCommand(command: string) {
let args = command.split(' ');
switch (args[0]) {
case "help":
this.addNotiMessage(`TStream - Streaming from terminal`);
this.addNotiMessage(`/name (name) - to set username`);
break;

case "name":
if (args.length == 2) {
let userConfig = this.getUserConfig()
if (userConfig == null) {
let color = constants.COLOR_LIST[Math.floor(Math.random() * (constants.COLOR_LIST.length))];
userConfig = {
name: args[1],
color: color,
}
} else {
userConfig.name = args[1]
}

this.setUserConfig(userConfig);
this.setState({userConfig: userConfig});
this.addNotiMessage(`Set name successfully to ${userConfig.name}`);

} else {
this.addNotiMessage("Invalid command");
}
break;
default:
this.addNotiMessage("Invalid command. Type /help to see available commands");
}

}

// display a notify for viewer only
addNotiMessage(messsage: string) {
let data = {
Name: '',
Content: messsage,
Color: '',
Time: new Date().toISOString(),
};

this.addNewMsg(data);

}

onSendMsg(content: string) {


getUserConfig(): TstreamUser | null {
const payload = localStorage.getItem(USER_CONFIG_KEY);
if (payload !== null) {
const tstreamUser : TstreamUser = JSON.parse(payload);
return tstreamUser
} else {
return null
}
}

setUserConfig(config: TstreamUser) {
localStorage.setItem(USER_CONFIG_KEY, JSON.stringify(config));
}

handleSendMsg(content: string) {
let tempMsg : string = content.trim();
let name : string = '';
let color : string = '';

// Don't find the user data in the browser
if (this.state.name === '' || this.state.color === '') {
let notification: string = '';
if (! this.state.userConfig) {

// ask for first time
if (!this.state.isWaitingUsername) {
notification = "Please enter your username (I.e: elonmusk)";
this.setState({
tempMsg: tempMsg,
isWaitingUsername: true,
});
}
else {
this.addNotiMessage("Please enter your username (I.e: elonmusk)");
return ;

} else {
// invalid username
if (tempMsg === '' || tempMsg.length > 10) {
notification = 'Invalid Username';
}
// valid username
else {
name = tempMsg;
color = constants.COLOR_LIST[Math.floor(Math.random() * (constants.COLOR_LIST.length))];
this.setState({
name: name,
color: color,
isWaitingUsername: false,
});
if (tempMsg.includes(" ") || tempMsg === '') {
this.addNotiMessage('Username must contain only lower case letters and number');
return ;

let tstreamUser : TstreamUser = {
} else {
// user just set username

this.addNotiMessage("You can change name again with command /name (newname)");
// valid username
let userConfig : TstreamUser = {
name: tempMsg,
color: color,
color:constants.COLOR_LIST[Math.floor(Math.random() * (constants.COLOR_LIST.length))],
}

localStorage.setItem('tstreamUser', JSON.stringify(tstreamUser));

tempMsg = this.state.tempMsg;
this.setState({
userConfig: userConfig,
isWaitingUsername: false,
});

// if the first message is empty, just ignore it
if (tempMsg === "") {
this.setState({
inputContent: "",
});
return ;
}
}
}
this.setUserConfig(userConfig);

// send notification
if (notification !== '') {
let data = {
Name: '',
Content: notification,
Color: '',
Time: new Date().toISOString(),
};
let data = {
Name: userConfig.name,
Content: this.state.tempMsg,
Color: userConfig.color,
Time: new Date().toISOString(),
};

this.addNewMsg(data);
return ;
this.addNewMsg(data);
this.props.msgManager?.pub(constants.MSG_TCHAT_OUT, data);
}
}
}

if (tempMsg === '') {
return;
}
} else {
let data = {
Name: this.state.userConfig.name,
Content: tempMsg,
Color: this.state.userConfig.color,
Time: new Date().toISOString(),
};

if (name === '') {
name = this.state.name;
this.addNewMsg(data);
this.props.msgManager?.pub(constants.MSG_TCHAT_OUT, data);
}
if (color === '') {
color = this.state.color;
}

let data = {
Name: name,
Content: tempMsg,
Color: color,
Time: new Date().toISOString(),
};

this.addNewMsg(data);
this.props.msgManager?.pub(constants.MSG_TREQUEST_CHAT, data);
}

render() {
Expand All @@ -210,22 +250,20 @@ class Chat extends React.Component<Props, State> {
<div style={{height: '0px'}} className="bg-black overflow-y-auto overflow-x-none p-2 flex flex-col-reverse flex-grow" id="chatbox">
{
this.state.msgList.slice(0).reverse().map(
(item, index) => <ChatSection Name={item.Name} Content={item.Content} Color={item.Color} Time={item.Time} key={index}/>
)
(item, index) => <ChatSection Name={item.Name} Content={item.Content} Color={item.Color} Time={item.Time} key={index}/>
)
}
</div>
<div className="bottom-0 transform w-full" id="textarea">
<div
className="border-b border-gray-500 flex-shrink-0 flex items-center justify-between"
>
<TextField
<div className="bottom-0 transform w-full" id="chat-input">
<div className="border-b border-gray-500 flex-shrink-0 flex items-center justify-between">
<TextField
InputProps={{
style: {
flexGrow: 1,
borderRadius: ".5rem",
backgroundColor: "rgba(75,85,99,1)",
fontFamily: "'Ubuntu Mono', monospace",
}
borderRadius: ".5rem",
backgroundColor: "rgba(75,85,99,1)",
fontFamily: "'Ubuntu Mono', monospace",
}
}}
placeholder={(this.state.isWaitingUsername) ? "Please enter your name..." : "Chat with everyone..."}
fullWidth
Expand Down
6 changes: 3 additions & 3 deletions client/src/components/StreamPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const StreamPreview: FC<Props> = ({ title, wsUrl, streamerID, nViewers, startedT

if (msg.Type === constants.MSG_TWRITE) {

var buffer = base64.toArrayBuffer(msg.Data)
var buffer = base64.str2ab(msg.Data)
tempMsg.pub(msg.Type, buffer);

} else if (msg.Type === constants.MSG_TWINSIZE) {
Expand All @@ -49,12 +49,12 @@ const StreamPreview: FC<Props> = ({ title, wsUrl, streamerID, nViewers, startedT

tempMsg.sub("request", (msgType: string) => {

var payload_byte = base64.toArrayBuffer(window.btoa(""));
var payload_byte = base64.str2ab(window.btoa(""));
var wrapper = JSON.stringify({
Type: msgType,
Data: Array.from(payload_byte),
});
const payload = base64.toArrayBuffer(window.btoa(wrapper))
const payload = base64.str2ab(window.btoa(wrapper))
util.sendWhenConnected(ws, payload);
})

Expand Down
4 changes: 2 additions & 2 deletions client/src/components/WSTerminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ class WSTerminal extends React.Component<Props, {}> {
this.rescale();
})

this.props.msgManager.pub("request", constants.MSG_TREQUEST_WINSIZE );
this.props.msgManager.pub("request", constants.MSG_TREQUEST_CACHE_MESSAGE );
this.props.msgManager.pub("request", constants.MSG_TREQUEST_WINSIZE);
this.props.msgManager.pub("request", constants.MSG_TREQUEST_CACHE_CONTENT);

window.addEventListener("resize", () => this.rescale());
this.rescale();
Expand Down
Loading

1 comment on commit 74f1003

@vercel
Copy link

@vercel vercel bot commented on 74f1003 Jul 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.