Merge pull request #28 from Reckless-Satoshi/asynchronous-chat

Minimal chat. Just enough for it to work. Does not log any message, so admin won't be able to resolve disputes easily with this set up.
This commit is contained in:
Reckless_Satoshi 2022-01-13 20:43:39 +00:00 committed by GitHub
commit 89c5f7a997
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 533 additions and 40 deletions

View File

@ -4,6 +4,8 @@ LND_CERT_BASE64=''
LND_MACAROON_BASE64=''
LND_GRPC_HOST='127.0.0.1:10009'
REDIS_URL=''
# Market price public API
MARKET_PRICE_API = 'https://blockchain.info/ticker'

View File

@ -375,7 +375,8 @@ class Logics():
# If there was no taker_bond object yet, generates one
order.last_satoshis = cls.satoshis_now(order)
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
description = f"RoboSats - Taking '{str(order)}' - This is a taker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel."
pos_text = 'Buying' if cls.is_buyer(order, user) else 'Selling'
description = f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {order.amount} - This is a taker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel."
# Gen hold Invoice
hold_payment = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600)

View File

@ -4,6 +4,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator, validat
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from django.utils.html import mark_safe
import uuid
from decouple import config
from pathlib import Path
@ -40,6 +41,7 @@ class LNPayment(models.Model):
# payment use details
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HOLD)
concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND)
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN)
@ -59,7 +61,7 @@ class LNPayment(models.Model):
receiver = models.ForeignKey(User, related_name='receiver', on_delete=models.CASCADE, null=True, default=None)
def __str__(self):
return (f'HTLC {self.id}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}')
return (f'LN-{str(self.id)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}')
class Order(models.Model):
@ -203,7 +205,7 @@ class MarketTick(models.Model):
maker and taker are commited with bonds (contract
is finished and cancellation has a cost)
'''
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
price = models.DecimalField(max_digits=10, decimal_places=2, default=None, null=True, validators=[MinValueValidator(0)])
volume = models.DecimalField(max_digits=8, decimal_places=8, default=None, null=True, validators=[MinValueValidator(0)])
premium = models.DecimalField(max_digits=5, decimal_places=2, default=None, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True)
@ -235,6 +237,6 @@ class MarketTick(models.Model):
tick.save()
def __str__(self):
return f'Tick: {self.id}'
return f'Tick: {str(self.id)[:8]}'

View File

@ -116,6 +116,7 @@ class OrderView(viewsets.ViewSet):
data['is_maker'] = order.maker == request.user
data['is_taker'] = order.taker == request.user
data['is_participant'] = data['is_maker'] or data['is_taker']
data['ur_nick'] = request.user.username
# 3) If not a participant and order is not public, forbid.
if not data['is_participant'] and order.status != Order.Status.PUB:

0
chat/__init__.py Normal file
View File

3
chat/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
chat/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ChatConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'chat'

64
chat/consumers.py Normal file
View File

@ -0,0 +1,64 @@
from channels.generic.websocket import AsyncWebsocketConsumer
from api.logics import Logics
from api.models import Order
import json
class ChatRoomConsumer(AsyncWebsocketConsumer):
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)
# # 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
print(self.user_nick)
print(self.order_id)
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
nick = text_data_json['nick']
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chatroom_message',
'message': message,
'nick': nick,
}
)
async def chatroom_message(self, event):
message = event['message']
nick = event['nick']
await self.send(text_data=json.dumps({
'message': message,
'user_nick': nick,
}))
pass

3
chat/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

6
chat/routing.py Normal file
View File

@ -0,0 +1,6 @@
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<order_id>\w+)/$', consumers.ChatRoomConsumer.as_asgi()),
]

View File

@ -0,0 +1,82 @@
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body>
<div class="container">
<div class="row d-flex justify-content-center">
<div class="col-6">
<form>
<div class="form-group">
<label for="exampleFormControlTextarea1" class="h4 pt-5">Chatroom</label>
<textarea class="form-control" id="chat-text" rows="10"></textarea><br>
</div>
<div class="form-group">
<input class="form-control" id="input" type="text"></br>
</div>
<input class="btn btn-secondary btn-lg btn-block" id="submit" type="button" value="Send">
</form>
</div>
</div>
</div>
{{ request.user.username|json_script:"user_username" }}
{{ order_id|json_script:"order-id" }}
<script>
const user_username = JSON.parse(document.getElementById('user_username').textContent);
document.querySelector('#submit').onclick = function (e) {
const messageInputDom = document.querySelector('#input');
const message = messageInputDom.value;
chatSocket.send(JSON.stringify({
'message': message,
'username': user_username,
}));
messageInputDom.value = '';
};
const orderId = JSON.parse(document.getElementById('order-id').textContent);
const chatSocket = new WebSocket(
'ws://' +
window.location.host +
'/ws/chat/' +
orderId +
'/'
);
chatSocket.onmessage = function (e) {
const data = JSON.parse(e.data);
console.log(data)
document.querySelector('#chat-text').value += (data.username + ': ' + data.message + '\n')
}
</script>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous">
</script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"
integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous">
</script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"
integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous">
</script>
</body>
</html>

3
chat/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

8
chat/urls.py Normal file
View File

@ -0,0 +1,8 @@
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('<str:order_id>/', views.room, name='order_chat'),
]

10
chat/views.py Normal file
View File

@ -0,0 +1,10 @@
from django.shortcuts import render
def index(request):
return render(request, 'index.html', {})
def room(request, order_id):
return render(request, 'chatroom.html', {
'order_id': order_id
})

View File

@ -2889,6 +2889,14 @@
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
},
"bufferutil": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz",
"integrity": "sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==",
"requires": {
"node-gyp-build": "^4.3.0"
}
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
@ -3319,6 +3327,15 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.19.tgz",
"integrity": "sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ=="
},
"d": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
"integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
"requires": {
"es5-ext": "^0.10.50",
"type": "^1.0.1"
}
},
"dayjs": {
"version": "1.10.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
@ -3564,6 +3581,35 @@
"integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==",
"dev": true
},
"es5-ext": {
"version": "0.10.53",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
"integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==",
"requires": {
"es6-iterator": "~2.0.3",
"es6-symbol": "~3.1.3",
"next-tick": "~1.0.0"
}
},
"es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
"requires": {
"d": "1",
"es5-ext": "^0.10.35",
"es6-symbol": "^3.1.1"
}
},
"es6-symbol": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
"integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
"requires": {
"d": "^1.0.1",
"ext": "^1.1.2"
}
},
"escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@ -3706,6 +3752,21 @@
}
}
},
"ext": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz",
"integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==",
"requires": {
"type": "^2.5.0"
},
"dependencies": {
"type": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz",
"integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw=="
}
}
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@ -4329,6 +4390,11 @@
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"dev": true
},
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
},
"is-windows": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
@ -5927,6 +5993,11 @@
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
},
"next-tick": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
},
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@ -5953,6 +6024,11 @@
"whatwg-url": "^5.0.0"
}
},
"node-gyp-build": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz",
"integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q=="
},
"node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@ -7902,11 +7978,24 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"type": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
"integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
},
"type-fest": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz",
"integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="
},
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"requires": {
"is-typedarray": "^1.0.0"
}
},
"uglify-es": {
"version": "3.3.9",
"resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
@ -8123,6 +8212,14 @@
"object-assign": "^4.1.1"
}
},
"utf-8-validate": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.8.tgz",
"integrity": "sha512-k4dW/Qja1BYDl2qD4tOMB9PFVha/UJtxTc1cXYOe3WwA/2m0Yn4qB7wLMpJyLJ/7DR0XnTut3HsCSzDT4ZvKgA==",
"requires": {
"node-gyp-build": "^4.3.0"
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -8305,6 +8402,34 @@
"integrity": "sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw==",
"dev": true
},
"websocket": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz",
"integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==",
"requires": {
"bufferutil": "^4.0.1",
"debug": "^2.2.0",
"es5-ext": "^0.10.50",
"typedarray-to-buffer": "^3.1.5",
"utf-8-validate": "^5.0.2",
"yaeti": "^0.0.6"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
"whatwg-fetch": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz",
@ -8440,6 +8565,11 @@
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
},
"yaeti": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
"integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
},
"yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",

View File

@ -33,6 +33,7 @@
"react-native": "^0.66.4",
"react-native-svg": "^12.1.1",
"react-qr-code": "^2.0.3",
"react-router-dom": "^5.2.0"
"react-router-dom": "^5.2.0",
"websocket": "^1.0.34"
}
}

View File

@ -0,0 +1,112 @@
import React, { Component } from 'react';
import { w3cwebsocket as W3CWebSocket } from "websocket";
import {Button, TextField, Link, Grid, Typography, Container, Card, CardHeader, Paper, Avatar} from "@mui/material";
import { withStyles } from "@mui/material";
export default class Chat extends Component {
constructor(props) {
super(props);
}
state = {
messages: [],
value:'',
orderId: 2,
};
client = new W3CWebSocket('ws://' + window.location.host + '/ws/chat/' + this.props.data.orderId + '/');
componentDidMount() {
this.client.onopen = () => {
console.log('WebSocket Client Connected')
console.log(this.props.data)
}
this.client.onmessage = (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,
}],
})
)
}
}
}
onButtonClicked = (e) => {
this.client.send(JSON.stringify({
type: "message",
message: this.state.value,
nick: this.props.data.urNick,
}));
this.state.value = ''
e.preventDefault();
}
render() {
return (
<Container component="main" maxWidth="xs">
<Paper style={{ height: 300, maxHeight: 300, overflow: 'auto', boxShadow: 'none', }}>
{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.data.urNick ?
<CardHeader
avatar={
<Avatar
alt={message.userNick}
src={window.location.origin +'/static/assets/avatars/' + message.userNick + '.png'}
/>
}
style={{backgroundColor: '#e8ffe6'}}
title={message.userNick}
subheader={message.msg}
/>
:
<CardHeader
avatar={
<Avatar
alt={message.userNick}
src={window.location.origin +'/static/assets/avatars/' + message.userNick + '.png'}
/>
}
style={{backgroundColor: '#fcfcfc'}}
title={message.userNick}
subheader={message.msg}
/>}
</Card>
</>)}
</Paper>
<form noValidate onSubmit={this.onButtonClicked}>
<Grid containter alignItems="stretch" style={{ display: "flex" }}>
<Grid item alignItems="stretch" style={{ display: "flex" }}>
<TextField
label="Type a message"
variant="outlined"
size="small"
value={this.state.value}
onChange={e => {
this.setState({ value: e.target.value });
this.value = this.state.value;
}}
/>
</Grid>
<Grid item alignItems="stretch" style={{ display: "flex" }}>
<Button type="submit" variant="contained" color="primary" > Send </Button>
</Grid>
</Grid>
</form>
</Container>
)
}
}

View File

@ -167,7 +167,7 @@ export default class MakerPage extends Component {
</RadioGroup>
</FormControl>
</Grid>
<Grid container xs={11} align="center">
<Grid containter xs={8} alignItems="stretch" style={{ display: "flex" }}>
<TextField
error={this.state.amount == 0}
helperText={this.state.amount == 0 ? 'Must be more than 0' : null}

View File

@ -94,6 +94,7 @@ export default class OrderPage extends Component {
satoshis: data.satoshis,
makerId: data.maker,
isParticipant: data.is_participant,
urNick: data.ur_nick,
makerNick: data.maker_nick,
takerId: data.taker,
takerNick: data.taker_nick,

View File

@ -2,6 +2,8 @@ import React, { Component } from "react";
import { Link, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider} from "@mui/material"
import QRCode from "react-qr-code";
import Chat from "./Chat"
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
@ -53,6 +55,7 @@ export default class TradeBox extends Component {
</Grid>
<Grid item xs={12} align="center">
<QRCode value={this.props.data.bondInvoice} size={305}/>
<Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.bondInvoice)}} align="center"> 📋Copy to clipboard</Button>
</Grid>
<Grid item xs={12} align="center">
<TextField
@ -61,9 +64,10 @@ export default class TradeBox extends Component {
size="small"
defaultValue={this.props.data.bondInvoice}
disabled="true"
helperText="This is a hold invoice. It will not be charged if the order succeeds or expires.
It will be charged if the order is cancelled or you lose a dispute."
helperText="This is a hold invoice. It will simply freeze in your wallet.
It will be charged only if you cancel the order or lose a dispute."
color = "secondary"
onClick = {this.copyCodeToClipboard}
/>
</Grid>
</Grid>
@ -90,6 +94,7 @@ export default class TradeBox extends Component {
</Grid>
<Grid item xs={12} align="center">
<QRCode value={this.props.data.escrowInvoice} size={305}/>
<Button size="small" color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.escrowInvoice)}} align="center"> 📋Copy to clipboard</Button>
</Grid>
<Grid item xs={12} align="center">
<TextField
@ -98,7 +103,7 @@ export default class TradeBox extends Component {
size="small"
defaultValue={this.props.data.escrowInvoice}
disabled="true"
helperText="This is a hold LN invoice. It will be charged once the buyer confirms he sent the fiat."
helperText="This is a hold invoice. It will simply freeze in your wallet. It will be charged once the buyer confirms he sent the fiat."
color = "secondary"
/>
</Grid>
@ -218,6 +223,7 @@ export default class TradeBox extends Component {
valid invoice for {pn(this.props.data.invoiceAmount)} Satoshis.
</Typography>
</Grid>
<form noValidate onSubmit={this.handleClickSubmitInvoiceButton}>
<Grid item xs={12} align="center">
<TextField
error={this.state.badInvoice}
@ -232,8 +238,9 @@ export default class TradeBox extends Component {
/>
</Grid>
<Grid item xs={12} align="center">
<Button variant='contained' color='primary' onClick={this.handleClickSubmitInvoiceButton}>Submit</Button>
<Button variant='contained' color='primary'>Submit</Button>
</Grid>
</form>
{this.showBondIsLocked()}
</Grid>
)
@ -324,7 +331,7 @@ handleRatingChange=(e)=>{
return(
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<Button defaultValue="confirm" variant='contained' color='primary' onClick={this.handleClickConfirmButton}>Confirm {this.props.data.currencyCode} sent</Button>
<Button defaultValue="confirm" variant='contained' color='secondary' onClick={this.handleClickConfirmButton}>Confirm {this.props.data.currencyCode} sent</Button>
</Grid>
</Grid>
)
@ -335,7 +342,7 @@ handleRatingChange=(e)=>{
// Ask for double confirmation.
return(
<Grid item xs={12} align="center">
<Button defaultValue="confirm" variant='contained' color='primary' onClick={this.handleClickConfirmButton}>Confirm {this.props.data.currencyCode} received</Button>
<Button defaultValue="confirm" variant='contained' color='secondary' onClick={this.handleClickConfirmButton}>Confirm {this.props.data.currencyCode} received</Button>
</Grid>
)
}
@ -357,24 +364,25 @@ handleRatingChange=(e)=>{
<b>Chatting with {this.props.data.isMaker ? this.props.data.takerNick : this.props.data.makerNick}</b>
</Typography>
</Grid>
<Grid item xs={12} align="left">
<Grid item xs={12} align="center">
{this.props.data.isSeller ?
<Typography component="body2" variant="body2">
Say hi to your peer robot! Be helpful and concise. Let him know how to send you {this.props.data.currencyCode}.
<Typography component="body2" variant="body2" align="center">
Say hi! Be helpful and concise. Let him know how to send you {this.props.data.currencyCode}.
</Typography>
:
<Typography component="body2" variant="body2">
Say hi to your peer robot! Ask for payment details and click 'Confirm {this.props.data.currencyCode} sent' as soon as you send the payment.
<Typography component="body2" variant="body2" align="center">
Say hi! Ask for payment details and click "Confirm Sent" as soon as the payment is sent.
</Typography>
}
<Divider/>
</Grid>
<Grid item xs={12} style={{ width:330, height:360}}>
CHAT PLACEHOLDER
</Grid>
<Chat data={this.props.data}/>
<Grid item xs={12} align="center">
{openDisputeButton ? this.showOpenDisputeButton() : ""}
{sendFiatButton ? this.showFiatSentButton() : ""}
{receivedFiatButton ? this.showFiatReceivedButton() : ""}
{openDisputeButton ? this.showOpenDisputeButton() : ""}
</Grid>
{this.showBondIsLocked()}
</Grid>

13
robosats/routing.py Normal file
View File

@ -0,0 +1,13 @@
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing
application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns,
# TODO add api.routing.websocket_urlpatterns when Order page works with websocket
)
),
})

View File

@ -39,7 +39,9 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'channels',
'api',
'chat',
'frontend.apps.FrontendConfig',
]
@ -120,6 +122,26 @@ USE_TZ = True
# https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_URL = 'static/'
ASGI_APPLICATION = "robosats.routing.application"
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [config('REDIS_URL')],
},
},
}
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": config('REDIS_URL'),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient"
}
}
}
# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field

View File

@ -19,5 +19,6 @@ from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')),
path('', include('frontend.urls'))
path('chat/', include('chat.urls')),
path('', include('frontend.urls')),
]

View File

@ -33,6 +33,19 @@ source /usr/local/bin/virtualenvwrapper.sh
## Install Django admin relational links
`pip install django-admin-relation-links`
## Install dependencies for websockets
Install Redis
`apt-get install redis-server`
Test redis-server
`redis-cli ping`
Install python dependencies
```
pip install channels
pip install django-redis
pip install channels-redis
```
*Django 4.0 at the time of writting*
### Launch the local development node
@ -100,6 +113,7 @@ npm install react-native-svg
npm install react-qr-code
npm install @mui/material
npm install react-markdown
npm install websocket
```
Note we are using mostly MaterialUI V5 (@mui/material) but Image loading from V4 (@material-ui/core) extentions (so both V4 and V5 are needed)