Improve chat status communication

This commit is contained in:
Reckless_Satoshi 2022-03-10 13:35:16 -08:00
parent 3d130129f1
commit 69f6735f86
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
12 changed files with 260 additions and 84 deletions

View File

@ -10,17 +10,16 @@ RoboSats is a simple and private way to exchange bitcoin for national currencies
<img width="75%" src="https://raw.githubusercontent.com/Reckless-Satoshi/robosats/main/frontend/static/assets/images/robosats_0.1.0_banner.png">
</div>
**Bitcoin mainnet:**
- Tor: robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion
- Url: robosats.com (Coming soon)
- Version: v0.1.0-mvp
### 🔗 **Bitcoin Mainnet**
- 🧅 **TOR URL:** [**RoboSats**6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion](http://robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion) ( Open with [Tor Browser](https://www.torproject.org/download/))
- Clearnet URL: [unsafe.robosats.com](https://unsafe.robosats.com) (not recommended!)
- Version: v0.1.0 MVP (+++)
**Bitcoin testnet:**
- Tor: robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion
- Url: testnet.robosats.com (Coming soon)
- Latest commit.
*⚠️ Always use [Tor Browser](https://www.torproject.org/download/) and .onion for best anonymity. The Clearnet URL redirects to a third party Tor2web service. Your privacy cannot be guaranteed to be respected. Use only to check around the app, never use for trading!⚠️*
*Always use [Tor Browser](https://www.torproject.org/download/) and .onion for best anonymity.*
You can also use RoboSats in Testnet:
- TOR URL: [RoboTestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion](http://robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion)
- Clearnet URL: [unsafe.testnet.robosats.com](https://unsafe.testnet.robosats.com)
## How to use it

View File

@ -2,7 +2,7 @@ from django.contrib import admin
from django_admin_relation_links import AdminChangeLinksMixin
from django.contrib.auth.models import Group, User
from django.contrib.auth.admin import UserAdmin
from .models import Order, LNPayment, Profile, MarketTick, Currency
from api.models import Order, LNPayment, Profile, MarketTick, Currency
admin.site.unregister(Group)
admin.site.unregister(User)

View File

@ -1,3 +1,20 @@
from django.contrib import admin
from django_admin_relation_links import AdminChangeLinksMixin
from chat.models import ChatRoom
# Register your models here.
@admin.register(ChatRoom)
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = (
"id",
"order_link",
"maker_link",
"taker_link",
"maker_connected",
"taker_connected",
"maker_connect_date",
"taker_connect_date",
"room_group_name",
)
change_links = ["order","maker","taker"]

View File

@ -1,57 +1,136 @@
from channels.generic.websocket import AsyncWebsocketConsumer
from api.logics import Logics
from channels.db import database_sync_to_async
from api.models import Order
from chat.models import ChatRoom
import json
class ChatRoomConsumer(AsyncWebsocketConsumer):
@database_sync_to_async
def allow_in_chatroom(self):
order = Order.objects.get(id=self.order_id)
if not (order.maker == self.user or order.taker == self.user):
print("Not allowed in this chat")
return False
return True
@database_sync_to_async
def save_connect_user(self):
'''Creates or updates the ChatRoom object'''
order = Order.objects.get(id=self.order_id)
if order.maker == self.user:
ChatRoom.objects.update_or_create(
id=self.order_id,
order=order,
room_group_name=self.room_group_name,
defaults={
"maker": self.user,
"maker_connected": True,
}
)
elif order.taker == self.user:
ChatRoom.objects.update_or_create(
id=self.order_id,
order=order,
room_group_name=self.room_group_name,
defaults={
"taker": self.user,
"taker_connected": True,
}
)
return None
@database_sync_to_async
def save_disconnect_user(self):
'''Creates or updates the ChatRoom object'''
order = Order.objects.get(id=self.order_id)
if order.maker == self.user:
ChatRoom.objects.update_or_create(
id=self.order_id,
defaults={
"maker_connected": False
}
)
elif order.taker == self.user:
ChatRoom.objects.update_or_create(
id=self.order_id,
defaults={
"taker_connected": False
}
)
return None
@database_sync_to_async
def is_peer_connected(self):
'''Creates or updates the ChatRoom object'''
chatroom = ChatRoom.objects.get(id=self.order_id)
if chatroom.maker == self.user:
return chatroom.taker_connected
if chatroom.taker == self.user:
return chatroom.maker_connected
async def connect(self):
self.order_id = self.scope["url_route"]["kwargs"]["order_id"]
self.room_group_name = f"chat_order_{self.order_id}"
self.user = self.scope["user"]
self.user_nick = str(self.user)
# Forbit if user is not part of the order
# Does not work Async
# order = Order.objects.get(id=self.order_id)
allowed = await self.allow_in_chatroom()
# # Check if user is participant on the order.
# if not (Logics.is_buyer(order[0], self.user) or Logics.is_seller(order[0], self.user)):
# print ("Outta this chat")
# return False
if allowed:
await self.save_connect_user()
await self.channel_layer.group_add(self.room_group_name,
self.channel_name)
await self.channel_layer.group_add(self.room_group_name,
self.channel_name)
await self.accept()
await self.accept()
async def disconnect(self, close_code):
await self.save_disconnect_user()
await self.channel_layer.group_discard(self.room_group_name,
self.channel_name)
await self.channel_layer.group_send(
self.room_group_name,
{
"type": "chatroom_message",
"message": 'peer-disconnected',
"nick": self.scope["user"].username,
"peer_connected": False,
},
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json["message"]
nick = text_data_json["nick"]
peer_connected = await self.is_peer_connected()
await self.channel_layer.group_send(
self.room_group_name,
{
"type": "chatroom_message",
"message": message,
"nick": nick,
"nick": self.scope["user"].username,
"peer_connected": peer_connected,
},
)
async def chatroom_message(self, event):
message = event["message"]
nick = event["nick"]
peer_connected = event["peer_connected"]
await self.send(text_data=json.dumps({
"message": message,
"user_nick": nick,
"peer_connected": peer_connected,
}))
pass

View File

@ -1,3 +1,43 @@
from django.db import models
from api.models import User, Order
# Create your models here.
class ChatRoom(models.Model):
'''
Simple ChatRoom model. Needed to facilitate communication: Is my counterpart in the room?
'''
id = models.PositiveBigIntegerField(primary_key=True, null=False,default=None, blank=True)
order = models.ForeignKey(
Order,
related_name="order",
on_delete=models.SET_NULL,
null=True,
default=None)
maker = models.ForeignKey(
User,
related_name="chat_maker",
on_delete=models.SET_NULL,
null=True,
default=None)
taker = models.ForeignKey(
User,
related_name="chat_taker",
on_delete=models.SET_NULL,
null=True,
default=None,
blank=True,
)
maker_connected = models.BooleanField(default=False, null=False)
taker_connected = models.BooleanField(default=False, null=False)
maker_connect_date = models.DateTimeField(auto_now_add=True)
taker_connect_date = models.DateTimeField(auto_now_add=True)
room_group_name = models.CharField(
max_length=50,
null=True,
default=None,
blank=True,
)

View File

@ -6583,6 +6583,11 @@
"resolve": "^1.9.0"
}
},
"reconnecting-websocket": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz",
"integrity": "sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng=="
},
"regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",

View File

@ -38,6 +38,7 @@
"react-qr-reader": "^2.2.1",
"react-responsive": "^9.0.0-beta.6",
"react-router-dom": "^5.2.0",
"reconnecting-websocket": "^4.4.0",
"websocket": "^1.0.34"
}
}

View File

@ -315,11 +315,11 @@ export default class BottomBar extends Component {
<ListItemIcon>
<PasswordIcon/>
</ListItemIcon>
<ListItemText secondary="Your token">
<ListItemText secondary="Back it up now. It will not remain here.">
{this.props.token ?
<TextField
disabled
label='Store Safely'
label='Your Token'
value={this.props.token }
variant='filled'
size='small'
@ -337,7 +337,7 @@ export default class BottomBar extends Component {
</ListItemText>
</ListItem>
<Divider><Chip label='Earn Sats'/></Divider>
<Divider><Chip label='Rewards & Compensations'/></Divider>
<ListItem>
<ListItemIcon>
<PersonAddAltIcon/>
@ -430,6 +430,7 @@ bottomBarDesktop =()=>{
<Grid item xs={1.9}>
<div style={{display: this.props.avatarLoaded ? '':'none'}}>
<ListItemButton onClick={this.handleClickOpenProfile} >
<Tooltip open={this.state.earned_rewards > 0 ? true: false} title="You can claim satoshis!">
<Tooltip open={(this.state.active_order_id > 0 & !this.state.profileShown & this.props.avatarLoaded) ? true: false}
title="You have an active order">
<ListItemAvatar sx={{ width: 30, height: 30 }} >
@ -444,6 +445,7 @@ bottomBarDesktop =()=>{
</Badge>
</ListItemAvatar>
</Tooltip>
</Tooltip>
<ListItemText primary={this.props.nickname}/>
</ListItemButton>
</div>
@ -671,8 +673,9 @@ bottomBarPhone =()=>{
<Grid item xs={1.6}>
<div style={{display: this.props.avatarLoaded ? '':'none'}}>
<Tooltip open={(this.state.active_order_id > 0 & !this.state.profileShown & this.props.avatarLoaded) ? true: false}
title="You have an active order">
<Tooltip open={this.state.earned_rewards > 0 ? true: false} title="You can claim satoshis!">
<Tooltip open={(this.state.active_order_id > 0 & !this.state.profileShown & this.props.avatarLoaded) ? true: false}
title="You have an active order">
<IconButton onClick={this.handleClickOpenProfile} sx={{margin: 0, bottom: 17, right: 8}} >
<Badge badgeContent={(this.state.active_order_id >0 & !this.state.profileShown) ? "": null} color="primary">
<Avatar className='phoneFlippedSmallAvatar'
@ -686,6 +689,7 @@ bottomBarPhone =()=>{
</Badge>
</IconButton>
</Tooltip>
</Tooltip>
</div>
</Grid>

View File

@ -1,7 +1,6 @@
import React, { Component } from 'react';
import { w3cwebsocket as W3CWebSocket } from "websocket";
import {Button, TextField, Grid, Container, Card, CardHeader, Paper, Avatar, FormHelperText} from "@mui/material";
import {Button, Badge, TextField, Grid, Container, Card, CardHeader, Paper, Avatar, FormHelperText, Typography} from "@mui/material";
import ReconnectingWebSocket from 'reconnecting-websocket';
export default class Chat extends Component {
constructor(props) {
@ -11,32 +10,52 @@ export default class Chat extends Component {
state = {
messages: [],
value:'',
connected: false,
peer_connected: false,
};
client = new W3CWebSocket('ws://' + window.location.host + '/ws/chat/' + this.props.orderId + '/');
rws = new ReconnectingWebSocket('ws://' + window.location.host + '/ws/chat/' + this.props.orderId + '/');
componentDidMount() {
this.client.onopen = () => {
console.log('WebSocket Client Connected')
}
this.client.onmessage = (message) => {
this.rws.addEventListener('open', () => {
console.log('Connected!');
this.setState({connected: true});
this.rws.send(JSON.stringify({
type: "message",
message: 'just-connected',
nick: this.props.ur_nick,
}));
});
this.rws.addEventListener('message', (message) => {
const dataFromServer = JSON.parse(message.data);
console.log('Got reply!', dataFromServer.type);
if (dataFromServer){
this.setState((state) =>
({
messages: [...state.messages,
{
msg: dataFromServer.message,
userNick: dataFromServer.user_nick,
}],
})
)
if (dataFromServer.message != 'just-connected' & dataFromServer.message != 'peer-disconnected'){
this.setState((state) =>
({
messages: [...state.messages,
{
msg: dataFromServer.message,
userNick: dataFromServer.user_nick,
}],
})
)
}
this.setState({peer_connected: dataFromServer.peer_connected})
}
});
}
this.rws.addEventListener('close', () => {
console.log('Socket is closed. Reconnect will be attempted');
this.setState({connected: false});
});
this.rws.addEventListener('error', () => {
console.error('Socket encountered error: Closing socket');
});
}
componentDidUpdate() {
@ -49,7 +68,7 @@ export default class Chat extends Component {
onButtonClicked = (e) => {
if(this.state.value!=''){
this.client.send(JSON.stringify({
this.rws.send(JSON.stringify({
type: "message",
message: this.state.value,
nick: this.props.ur_nick,
@ -62,17 +81,38 @@ export default class Chat extends Component {
render() {
return (
<Container component="main" maxWidth="xs">
<Paper style={{ height: 300, maxHeight: 300, overflow: 'auto', boxShadow: 'none', }}>
<Paper elevation={1} style={{ height: 300, maxHeight: 300, overflow: 'auto', backgroundColor: '#F7F7F7' }}>
<Grid container xs={12} spacing={0.5}>
<Grid item xs={0.5}/>
<Grid item xs={5}>
<Paper elevation={1} style={this.state.connected ? {backgroundColor: '#e8ffe6'}: {backgroundColor: '#FFF1C5'}}>
<Typography variant='caption' >
You: {this.state.connected ? 'connected': 'disconnected'}
</Typography>
</Paper>
</Grid>
<Grid item xs={1}/>
<Grid item xs={5}>
<Paper elevation={1} style={this.state.peer_connected ? {backgroundColor: '#e8ffe6'}: {backgroundColor: '#FFF1C5'}}>
<Typography variant='caption'>
Peer: {this.state.peer_connected ? 'connected': 'disconnected'}
</Typography>
</Paper>
</Grid>
<Grid item xs={0.5}/>
</Grid>
{this.state.messages.map(message => <>
<Card elevation={5} align="left" >
{/* If message sender is not our nick, gray color, if it is our nick, green color */}
{message.userNick == this.props.ur_nick ?
<CardHeader
avatar={
<Avatar
alt={message.userNick}
src={window.location.origin +'/static/assets/avatars/' + message.userNick + '.png'}
/>
<Badge variant="dot" overlap="circular" badgeContent="" color={this.state.connected ? "success" : "error"}>
<Avatar className="flippedSmallAvatar"
alt={message.userNick}
src={window.location.origin +'/static/assets/avatars/' + message.userNick + '.png'}
/>
</Badge>
}
style={{backgroundColor: '#e8ffe6'}}
title={message.userNick}
@ -82,10 +122,12 @@ export default class Chat extends Component {
:
<CardHeader
avatar={
<Avatar
alt={message.userNick}
src={window.location.origin +'/static/assets/avatars/' + message.userNick + '.png'}
/>
<Badge variant="dot" overlap="circular" badgeContent="" color={this.state.peer_connected ? "success" : "error"}>
<Avatar className="flippedSmallAvatar"
alt={message.userNick}
src={window.location.origin +'/static/assets/avatars/' + message.userNick + '.png'}
/>
</Badge>
}
style={{backgroundColor: '#fcfcfc'}}
title={message.userNick}
@ -98,20 +140,22 @@ export default class Chat extends Component {
</Paper>
<form noValidate onSubmit={this.onButtonClicked}>
<Grid containter alignItems="stretch" style={{ display: "flex" }}>
<Grid item alignItems="stretch" style={{ display: "flex" }}>
<Grid item alignItems="stretch" style={{ display: "flex"}}>
<TextField
label="Type a message"
variant="outlined"
variant="standard"
size="small"
helperText={this.state.connected ? null : "Connecting..."}
value={this.state.value}
onChange={e => {
this.setState({ value: e.target.value });
this.value = this.state.value;
}}
sx={{width: 214}}
/>
</Grid>
<Grid item alignItems="stretch" style={{ display: "flex" }}>
<Button type="submit" variant="contained" color="primary" > Send </Button>
<Button disabled={!this.state.connected} type="submit" variant="contained" color="primary" > Send </Button>
</Grid>
</Grid>
</form>

View File

@ -790,7 +790,6 @@ handleRatingRobosatsChange=(e)=>{
Say hi! Ask for payment details and click "Confirm Sent" as soon as the payment is sent.
</Typography>
}
<Divider/>
</Grid>
<Chat orderId={this.props.data.id} ur_nick={this.props.data.ur_nick}/>

File diff suppressed because one or more lines are too long

View File

@ -58,10 +58,6 @@
!*** ./src/components/UserGenPage.js ***!
\***************************************/
/*!****************************************!*\
!*** ./node_modules/es5-ext/global.js ***!
\****************************************/
/*!****************************************!*\
!*** ./node_modules/qr.js/lib/math.js ***!
\****************************************/
@ -146,10 +142,6 @@
!*** ./node_modules/stylis/src/Prefixer.js ***!
\*********************************************/
/*!*********************************************!*\
!*** ./node_modules/websocket/package.json ***!
\*********************************************/
/*!**********************************************!*\
!*** ./node_modules/@mui/system/esm/grid.js ***!
\**********************************************/
@ -202,14 +194,6 @@
!*** ./node_modules/stylis/src/Serializer.js ***!
\***********************************************/
/*!***********************************************!*\
!*** ./node_modules/websocket/lib/browser.js ***!
\***********************************************/
/*!***********************************************!*\
!*** ./node_modules/websocket/lib/version.js ***!
\***********************************************/
/*!************************************************!*\
!*** ./node_modules/@mui/system/esm/sizing.js ***!
\************************************************/
@ -2226,6 +2210,10 @@
!*** ./node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.js ***!
\********************************************************************************/
/*!********************************************************************************!*\
!*** ./node_modules/reconnecting-websocket/dist/reconnecting-websocket-mjs.js ***!
\********************************************************************************/
/*!*********************************************************************************!*\
!*** ./node_modules/@babel/runtime/helpers/esm/objectWithoutPropertiesLoose.js ***!
\*********************************************************************************/