diff --git a/lnbits/extensions/diagonalley/crud.py b/lnbits/extensions/diagonalley/crud.py index f093ba69..8dadb739 100644 --- a/lnbits/extensions/diagonalley/crud.py +++ b/lnbits/extensions/diagonalley/crud.py @@ -408,8 +408,10 @@ async def create_diagonalley_market_stalls( async def update_diagonalley_market(market_id): pass + ### CHAT / MESSAGES + async def create_chat_message(data: CreateChatMessage): print("DATA", data) await db.execute( @@ -441,3 +443,14 @@ async def get_diagonalley_chat_messages(room_name: str): ) return [ChatMessage(**row) for row in rows] + + +async def get_diagonalley_chat_by_merchant(ids: List[str]) -> List[ChatMessage]: + + q = ",".join(["?"] * len(ids)) + rows = await db.fetchall( + f"SELECT * FROM diagonalley.messages WHERE id_conversation IN ({q})", + (*ids,), + ) + print(ids, q, rows) + return [ChatMessage(**row) for row in rows] diff --git a/lnbits/extensions/diagonalley/notifier.py b/lnbits/extensions/diagonalley/notifier.py index e21be500..08badfc7 100644 --- a/lnbits/extensions/diagonalley/notifier.py +++ b/lnbits/extensions/diagonalley/notifier.py @@ -78,9 +78,9 @@ class Notifier: async def _notify(self, message: str, room_name: str): """Notifier""" - d = json.loads(message) d["room_name"] = room_name + print("hey", d) db_msg = CreateChatMessage.parse_obj(d) print("NOT:", db_msg) await create_chat_message(data=db_msg) diff --git a/lnbits/extensions/diagonalley/templates/diagonalley/index.html b/lnbits/extensions/diagonalley/templates/diagonalley/index.html index 5ad6f6a5..f898fd40 100644 --- a/lnbits/extensions/diagonalley/templates/diagonalley/index.html +++ b/lnbits/extensions/diagonalley/templates/diagonalley/index.html @@ -345,20 +345,6 @@
- + Product List a product - + Product List a product + Shipping Zone Create a shipping zone @@ -382,7 +368,26 @@ Create a market stall to list products on - + Product List a product + + Product List a product + Create Market Makes a simple frontend shop for your stalls (not NOSTR) + {{ col.label }} @@ -434,6 +440,23 @@ :icon="props.expand ? 'remove' : 'add'" /> + + + + + {{ col.value }} @@ -824,22 +847,114 @@ {% include "diagonalley/_api_docs.html" %} +
Messages
- -
-
+ + + + +
+
+ +
+ +
+
+ + + + + +
+ +
- + {% endblock %} {% block scripts %} {{ window_vars(user) }} @@ -873,6 +987,15 @@ const pica = window.pica() + function imgSizeFit(img, maxWidth = 1024, maxHeight = 768) { + let ratio = Math.min( + 1, + maxWidth / img.naturalWidth, + maxHeight / img.naturalHeight + ) + return {width: img.naturalWidth * ratio, height: img.naturalHeight * ratio} + } + const mapStalls = obj => { obj._data = _.clone(obj) return obj @@ -891,6 +1014,7 @@ new Date(obj.time * 1000), 'YYYY-MM-DD HH:mm' ) + // obj.unread = false return obj } const mapKeys = obj => { @@ -933,8 +1057,12 @@ customerKeys: [], customerKey: '', customerMessages: {}, + messages: {}, + newMessage: '', + orderMessages: {}, shippedModel: false, shippingZoneOptions: [ + 'Free (digital)', 'Worldwide', 'Europe', 'Australia', @@ -999,17 +1127,17 @@ ordersTable: { columns: [ /*{ - name: 'product', - align: 'left', - label: 'Product', - field: 'product' - }, - { - name: 'quantity', - align: 'left', - label: 'Quantity', - field: 'quantity' - },*/ + name: 'product', + align: 'left', + label: 'Product', + field: 'product' + }, + { + name: 'quantity', + align: 'left', + label: 'Quantity', + field: 'quantity' + },*/ { name: 'id', align: 'left', @@ -1443,9 +1571,10 @@ let image = new Image() image.src = blobURL image.onload = async () => { + let fit = imgSizeFit(image) let canvas = document.createElement('canvas') - canvas.setAttribute('width', 760) - canvas.setAttribute('height', 490) + canvas.setAttribute('width', fit.width) + canvas.setAttribute('height', fit.height) await pica.resize(image, canvas, { quality: 0, alpha: true, @@ -1657,7 +1786,7 @@ .then(response => { if (response.data) { this.markets = response.data.map(mapMarkets) - console.log(this.markets) + // console.log(this.markets) } }) .catch(error => { @@ -1756,10 +1885,10 @@ //////////////////////////////////////// ////////////////ORDERS////////////////// //////////////////////////////////////// - getOrders: function () { + getOrders: async function () { var self = this - LNbits.api + await LNbits.api .request( 'GET', '/diagonalley/api/v1/orders?all_wallets=true', @@ -1768,7 +1897,6 @@ .then(function (response) { if (response.data) { self.orders = response.data.map(mapOrders) - console.log(self.orders) } }) .catch(function (error) { @@ -1839,21 +1967,190 @@ }, exportOrdersCSV: function () { LNbits.utils.exportCSV(this.ordersTable.columns, this.orders) + }, + /// CHAT + async getAllMessages() { + await LNbits.api + .request( + 'GET', + `/diagonalley/api/v1/chat/messages/merchant?orders=${this.orders + .map(o => o.invoiceid) + .toString()}`, + this.g.user.wallets[0].adminkey + ) + .then(res => { + this.messages = _.groupBy(res.data, 'id_conversation') + this.checkUnreadMessages() + console.log('Get new messages!') + }) + .catch(error => { + LNbits.utils.notifyApiError(error) + }) + }, + updateLastSeenMsg(id) { + let data = this.$q.localStorage.getItem( + `lnbits.diagonalley.${this.g.user.id}` + ) + let chat = { + ...data.chat, + [`${id}`]: { + timestamp: Object.keys(this.orderMessages)[ + Object.keys(this.orderMessages).length - 1 + ] + } + } + console.log({chat}) + this.$q.localStorage.set(`lnbits.diagonalley.${this.g.user.id}`, { + ...data, + chat + }) + this.checkUnreadMessages() + }, + checkUnreadMessages() { + let lastMsgs = this.$q.localStorage.getItem( + `lnbits.diagonalley.${this.g.user.id}` + ).chat + for (let key in this.messages) { + let idx = this.orders.findIndex(f => f.invoiceid == key) + if (!lastMsgs[key]) { + this.updateLastSeenMsg(key) + //this.orders[idx].unread = true + return + } + console.log( + 'Key', + key, + 'saved:', + lastMsgs[key].timestamp, + 'messages: ', + Math.max(...this.messages[key].map(c => c.timestamp)), + lastMsgs[key].timestamp < + Math.max(...this.messages[key].map(c => c.timestamp)) + ) + if ( + lastMsgs[key].timestamp < + Math.max(...this.messages[key].map(c => c.timestamp)) + ) { + this.$set(this.orders[idx], 'unread', true) + // this.orders[idx].unread = true + } else { + this.$set(this.orders[idx], 'unread', false) + // this.orders[idx].unread = false + } + console.log('Order:', this.orders[idx]) + } + }, + clearMessage() { + this.newMessage = '' + this.$refs.newMessage.focus() + }, + sendMessage() { + let message = { + msg: this.newMessage, + pubkey: this.keys.pubkey + } + this.ws.send(JSON.stringify(message)) + + this.clearMessage() + }, + chatRoom(id) { + this.startChat(id) + this.orderMessages = {} + this.messages[id].map(m => { + this.$set(this.orderMessages, m.timestamp, { + msg: m.msg, + pubkey: m.pubkey + }) + }) + this.$refs.chatCard.scrollIntoView({ + behavior: 'smooth', + inline: 'nearest' + }) + this.updateLastSeenMsg(id) + //"ea2fbf6c91aa228603681e2cc34bb06e34e6d1375fa4d6c35756182b2fa3307f" + //"c7435a04875c26e28db91a377bd6e991dbfefeefea8258415f3ae0c716ed2335" + }, + startChat(room_name) { + if (this.ws) { + this.ws.close() + } + if (location.protocol == 'https:') { + ws_scheme = 'wss://' + } else { + ws_scheme = 'ws://' + } + ws = new WebSocket( + ws_scheme + location.host + '/diagonalley/ws/' + room_name + ) + + function checkWebSocket(event) { + if (ws.readyState === WebSocket.CLOSED) { + console.log('WebSocket CLOSED: Reopening') + ws = new WebSocket( + ws_scheme + location.host + '/diagonalley/ws/' + room_name + ) + } + } + + ws.onmessage = event => { + let event_data = JSON.parse(event.data) + + this.$set(this.orderMessages, Date.now(), event_data) + this.updateLastSeenMsg(room_name) + } + + ws.onclose = event => { + this.updateLastSeenMsg(room_name) + } + + this.ws = ws } }, - created: function () { + async created() { if (this.g.user.wallets.length) { this.getStalls() this.getProducts() this.getZones() - this.getOrders() + await this.getOrders() this.getMarkets() - this.customerKeys = [ - 'cb4c0164fe03fcdadcbfb4f76611c71620790944c24f21a1cd119395cdedfe1b', - 'a9c17358a6dc4ceb3bb4d883eb87967a66b3453a0f3199f0b1c8eef8070c6a07' - ] + await this.getAllMessages() + let keys = this.$q.localStorage.getItem( + `lnbits.diagonalley.${this.g.user.id}` + ) + if (keys) { + this.keys = keys + } + setInterval(() => { + this.getAllMessages() + }, 300000) } } }) + {% endblock %} diff --git a/lnbits/extensions/diagonalley/templates/diagonalley/order.html b/lnbits/extensions/diagonalley/templates/diagonalley/order.html index aa9d0de4..81cdfc36 100644 --- a/lnbits/extensions/diagonalley/templates/diagonalley/order.html +++ b/lnbits/extensions/diagonalley/templates/diagonalley/order.html @@ -68,7 +68,7 @@ dense emit-value v-model="selectedOrder" - :options="user.orders" + :options="Object.keys(user.orders)" label="Order" hint="Select an order from this merchant" @input="val => { changeOrder() }" @@ -187,7 +187,7 @@ msg: this.newMessage, pubkey: this.user.keys.publickey } - ws.send(JSON.stringify(message)) + this.ws.send(JSON.stringify(message)) this.clearMessage() }, @@ -236,10 +236,16 @@ LNbits.utils.notifyApiError(error) }) }, - changeOrder() { - console.log(this.selectedOrder) + async changeOrder() { + this.products = this.user.orders[this.selectedOrder] + this.messages = {} + await this.getMessages(this.selectedOrder) + this.startChat(this.selectedOrder) }, startChat(room_name) { + if (this.ws) { + this.ws.close() + } if (location.protocol == 'https:') { ws_scheme = 'wss://' } else { @@ -268,51 +274,51 @@ } }, async created() { - this.stall = JSON.parse('{{ stall | tojson }}') let order_details = JSON.parse('{{ order | tojson }}') let products = JSON.parse('{{ products | tojson }}') - let order_id = '{{ order_id }}' + this.stall = JSON.parse('{{ stall | tojson }}') + this.products = order_details.map(o => { + let product = products.find(p => p.id == o.product_id) + return { + quantity: o.quantity, + name: product.product, + image: product.image, + price: product.price + } + }) + let data = this.$q.localStorage.getItem(`lnbits.diagonalley.data`) try { if (data) { this.user = data //add chat key (merchant pubkey) if not set - if (!this.user.chats[`${order_id}`]) { - this.$set(this.user.chats, order_id, []) + if (!this.user.orders[`${order_id}`]) { + this.$set(this.user.orders, order_id, this.products) } //this.$q.localStorage.set(`lnbits.diagonalley.data`, this.user) } else { // generate keys await this.generateKeys() // populate user data - this.user.chats = { - [`${order_id}`]: [] + this.user.orders = { + [`${order_id}`]: this.products } - this.user.orders = [] + //this.user.orders = [] } - this.order_details = order_details - this.products = order_details.map(o => { - let product = products.find(p => p.id == o.product_id) - return { - quantity: o.quantity, - name: product.product, - image: product.image, - price: product.price - } - }) + //this.order_details = order_details - this.user.orders = [...new Set([...this.user.orders, order_id])] + //this.user.orders = [...new Set([...this.user.orders, order_id])] this.selectedOrder = order_id await this.getMessages(order_id) this.$q.localStorage.set(`lnbits.diagonalley.data`, this.user) this.startChat(order_id) - console.log(this.products) + console.log(this.messages) } catch (e) { console.error(e) } diff --git a/lnbits/extensions/diagonalley/templates/diagonalley/stall.html b/lnbits/extensions/diagonalley/templates/diagonalley/stall.html index a3a04b1e..05163573 100644 --- a/lnbits/extensions/diagonalley/templates/diagonalley/stall.html +++ b/lnbits/extensions/diagonalley/templates/diagonalley/stall.html @@ -125,7 +125,7 @@ >
- {{cat}}
@@ -409,8 +409,18 @@ if (res.data.paid) { this.$q.notify({ type: 'positive', - message: 'Sats received, thanks!', - icon: 'thumb_up' + multiLine: true, + message: + "Sats received, thanks! You'l be redirected to the order page...", + icon: 'thumb_up', + actions: [ + { + label: 'See Order', + handler: () => { + window.location.href = `/diagonalley/order/?merch=${this.stall.id}&invoice_id=${this.qrCodeDialog.data.payment_hash}` + } + } + ] }) clearInterval(this.qrCodeDialog.paymentChecker) this.resetCart() diff --git a/lnbits/extensions/diagonalley/views.py b/lnbits/extensions/diagonalley/views.py index 27875287..2bf98211 100644 --- a/lnbits/extensions/diagonalley/views.py +++ b/lnbits/extensions/diagonalley/views.py @@ -94,7 +94,9 @@ async def display(request: Request, market_id): @diagonalley_ext.get("/order", response_class=HTMLResponse) -async def chat_page(request: Request, merch: str = Query(...), invoice_id: str = Query(...)): +async def chat_page( + request: Request, merch: str = Query(...), invoice_id: str = Query(...) +): stall = await get_diagonalley_stall(merch) order = await get_diagonalley_order_invoiceid(invoice_id) _order = await get_diagonalley_order_details(order.id) @@ -110,9 +112,9 @@ async def chat_page(request: Request, merch: str = Query(...), invoice_id: str = "publickey": stall.publickey, "wallet": stall.wallet, }, - "order_id": order.id, + "order_id": order.invoiceid, "order": [details.dict() for details in _order], - "products": [product.dict() for product in products] + "products": [product.dict() for product in products], }, ) @@ -123,6 +125,41 @@ async def chat_page(request: Request, merch: str = Query(...), invoice_id: str = notifier = Notifier() +# class ConnectionManager: +# def __init__(self): +# self.active_connections: List[WebSocket] = [] + +# async def connect(self, websocket: WebSocket, room_name: str): +# await websocket.accept() +# websocket.id = room_name +# self.active_connections.append(websocket) + +# def disconnect(self, websocket: WebSocket): +# self.active_connections.remove(websocket) + +# async def send_personal_message(self, message: str, room_name: str): +# for connection in self.active_connections: +# if connection.id == room_name: +# await connection.send_text(message) + +# async def broadcast(self, message: str): +# for connection in self.active_connections: +# await connection.send_text(message) + + +# manager = ConnectionManager() + + +# @diagonalley_ext.websocket("/ws/{room_name}") +# async def websocket_endpoint(websocket: WebSocket, room_name: str): +# await manager.connect(websocket, room_name) +# try: +# while True: +# data = await websocket.receive_text() +# except WebSocketDisconnect: +# manager.disconnect(websocket) + + @diagonalley_ext.websocket("/ws/{room_name}") async def websocket_endpoint( websocket: WebSocket, room_name: str, background_tasks: BackgroundTasks @@ -143,7 +180,7 @@ async def websocket_endpoint( if websocket not in room_members: print("Sender not in room member: Reconnecting...") await notifier.connect(websocket, room_name) - + print("ENDPOINT", data) await notifier._notify(data, room_name) except WebSocketDisconnect: diff --git a/lnbits/extensions/diagonalley/views_api.py b/lnbits/extensions/diagonalley/views_api.py index f84ea8e9..2f375357 100644 --- a/lnbits/extensions/diagonalley/views_api.py +++ b/lnbits/extensions/diagonalley/views_api.py @@ -1,10 +1,10 @@ from base64 import urlsafe_b64encode from http import HTTPStatus -from typing import List +from typing import List, Union from uuid import uuid4 from fastapi import Request -from fastapi.param_functions import Query +from fastapi.param_functions import Body, Query from fastapi.params import Depends from loguru import logger from secp256k1 import PrivateKey, PublicKey @@ -34,6 +34,7 @@ from .crud import ( delete_diagonalley_product, delete_diagonalley_stall, delete_diagonalley_zone, + get_diagonalley_chat_by_merchant, get_diagonalley_chat_messages, get_diagonalley_latest_chat_messages, get_diagonalley_market, @@ -255,6 +256,14 @@ async def api_diagonalley_orders( return {"message": "We could not retrieve the orders."} +@diagonalley_ext.get("/api/v1/orders/{order_id}") +async def api_diagonalley_order_by_id(order_id: str): + order = (await get_diagonalley_order(order_id)).dict() + order["details"] = await get_diagonalley_order_details(order_id) + + return order + + @diagonalley_ext.post("/api/v1/orders") async def api_diagonalley_order_create(data: createOrder): ref = urlsafe_short_hash() @@ -488,6 +497,16 @@ async def api_diagonalley_generate_keys(): ## MESSAGES/CHAT +@diagonalley_ext.get("/api/v1/chat/messages/merchant") +async def api_get_merchant_messages( + orders: str = Query(...), wallet: WalletTypeInfo = Depends(require_admin_key) +): + + return [ + msg.dict() for msg in await get_diagonalley_chat_by_merchant(orders.split(",")) + ] + + @diagonalley_ext.get("/api/v1/chat/messages/{room_name}") async def api_get_latest_chat_msg(room_name: str, all_messages: bool = Query(False)): if all_messages: