Frontend works
This commit is contained in:
parent
a572ca0d7c
commit
73bfff6a0f
13 changed files with 5942 additions and 13 deletions
|
@ -67,7 +67,7 @@ fn load_db(conn: &Connection) -> SQLResult<DBLog> {
|
||||||
channel_name: if userchname != None {
|
channel_name: if userchname != None {
|
||||||
format!("#{}", userchname.unwrap_or("<unknown>".to_string()))
|
format!("#{}", userchname.unwrap_or("<unknown>".to_string()))
|
||||||
} else {
|
} else {
|
||||||
format!("@{}", channelname.unwrap_or("<unknown>".to_string()))
|
channelname.unwrap_or("<unknown>".to_string())
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
|
@ -54,6 +54,9 @@ struct Workspace {
|
||||||
|
|
||||||
#[graphql(description = "URL to workspace icon")]
|
#[graphql(description = "URL to workspace icon")]
|
||||||
icon: String,
|
icon: String,
|
||||||
|
|
||||||
|
#[graphql(description = "List of channels and private chats")]
|
||||||
|
channels: Vec<Channel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, juniper::GraphQLObject)]
|
#[derive(Debug, juniper::GraphQLObject)]
|
||||||
|
@ -104,7 +107,12 @@ impl juniper::Context for Context {}
|
||||||
|
|
||||||
/// Get message id for slack message
|
/// Get message id for slack message
|
||||||
fn message_id(msg: &DBMessage) -> juniper::ID {
|
fn message_id(msg: &DBMessage) -> juniper::ID {
|
||||||
juniper::ID::new(format!("{}/{}", msg.channel_name, msg.time.timestamp()))
|
juniper::ID::new(format!(
|
||||||
|
"{}@{}/{}",
|
||||||
|
msg.username,
|
||||||
|
msg.channel_name,
|
||||||
|
msg.time.timestamp()
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert from DB struct to GQL
|
/// Convert from DB struct to GQL
|
||||||
|
@ -137,17 +145,6 @@ impl Query {
|
||||||
"1.0"
|
"1.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn workspace(context: &Context) -> FieldResult<Vec<Workspace>> {
|
|
||||||
let mut results = vec![];
|
|
||||||
for ws in context.databases.as_slice() {
|
|
||||||
results.push(Workspace {
|
|
||||||
name: ws.name.clone(),
|
|
||||||
icon: ws.icon.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Ok(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn channels(context: &Context, workspace: String) -> FieldResult<Vec<Channel>> {
|
fn channels(context: &Context, workspace: String) -> FieldResult<Vec<Channel>> {
|
||||||
let dbs = context
|
let dbs = context
|
||||||
.databases
|
.databases
|
||||||
|
@ -173,6 +170,28 @@ impl Query {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn workspace(context: &Context) -> FieldResult<Vec<Workspace>> {
|
||||||
|
let mut results = vec![];
|
||||||
|
for ws in context.databases.as_slice() {
|
||||||
|
let mut channels = HashSet::new();
|
||||||
|
for msg in &ws.messages {
|
||||||
|
channels.insert(msg.channel_name.clone());
|
||||||
|
}
|
||||||
|
results.push(Workspace {
|
||||||
|
name: ws.name.clone(),
|
||||||
|
icon: ws.icon.clone(),
|
||||||
|
channels: channels
|
||||||
|
.iter()
|
||||||
|
.map(|name| Channel {
|
||||||
|
name: name.clone(),
|
||||||
|
is_private: !name.starts_with("#"),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
fn messages(
|
fn messages(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
workspace: String,
|
workspace: String,
|
||||||
|
|
14
frontend/index.html
Normal file
14
frontend/index.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Ripcord log viewer</title>
|
||||||
|
<link rel="stylesheet" href="style/layout.scss" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<main></main>
|
||||||
|
<script src="index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
48
frontend/index.tsx
Normal file
48
frontend/index.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import React from "react";
|
||||||
|
import { render } from "react-dom";
|
||||||
|
|
||||||
|
import { ApolloProvider } from "@apollo/react-hooks";
|
||||||
|
import ApolloClient, { gql } from "apollo-boost";
|
||||||
|
import { InMemoryCache } from "apollo-cache-inmemory";
|
||||||
|
|
||||||
|
import App from "./src/App";
|
||||||
|
|
||||||
|
const cache = new InMemoryCache();
|
||||||
|
const client = new ApolloClient({
|
||||||
|
cache,
|
||||||
|
resolvers: {
|
||||||
|
Mutation: {
|
||||||
|
gotoChat: (_root, currentChat, { cache }) => {
|
||||||
|
const query = gql`
|
||||||
|
query GotoChatQuery {
|
||||||
|
currentChat {
|
||||||
|
workspace
|
||||||
|
channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
currentChat.__typename = "ChatInfo";
|
||||||
|
cache.writeQuery({
|
||||||
|
query,
|
||||||
|
data: {
|
||||||
|
currentChat
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
uri: "http://localhost:8080/graphql"
|
||||||
|
});
|
||||||
|
cache.writeData({
|
||||||
|
data: {
|
||||||
|
currentChat: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ApolloProvider client={client}>
|
||||||
|
<App />
|
||||||
|
</ApolloProvider>,
|
||||||
|
document.querySelector("main")
|
||||||
|
);
|
26
frontend/package.json
Normal file
26
frontend/package.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "parcel index.html",
|
||||||
|
"build": "parcel build index.html"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@apollo/react-hooks": "^3.1.3",
|
||||||
|
"@types/react": "^16.9.19",
|
||||||
|
"@types/react-dom": "^16.9.5",
|
||||||
|
"@types/react-router-dom": "^5.1.3",
|
||||||
|
"apollo-boost": "^0.4.7",
|
||||||
|
"apollo-cache-inmemory": "^1.6.5",
|
||||||
|
"graphql": "^14.5.8",
|
||||||
|
"parcel-bundler": "^1.12.4",
|
||||||
|
"react": "^16.8.3",
|
||||||
|
"react-dom": "^16.12.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"sass": "^1.25.0",
|
||||||
|
"typescript": "^3.7.5"
|
||||||
|
}
|
||||||
|
}
|
16
frontend/src/App.tsx
Normal file
16
frontend/src/App.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import React from "react";
|
||||||
|
import ChannelList from "./ChannelList";
|
||||||
|
import Chatroom from "./ChatRoom";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<div className="main">
|
||||||
|
<section className="channelList">
|
||||||
|
<ChannelList />
|
||||||
|
</section>
|
||||||
|
<section className="chatroom">
|
||||||
|
<Chatroom />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
69
frontend/src/ChannelList.tsx
Normal file
69
frontend/src/ChannelList.tsx
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useQuery, useMutation } from "@apollo/react-hooks";
|
||||||
|
import { gql } from "apollo-boost";
|
||||||
|
import { GQLWorkspace, GQLChannel } from "./gql-types";
|
||||||
|
|
||||||
|
function channelsFirst(a: GQLChannel, b: GQLChannel): number {
|
||||||
|
let sameStatus = a.isPrivate == b.isPrivate;
|
||||||
|
if (!sameStatus) {
|
||||||
|
return a.isPrivate ? 1 : -1;
|
||||||
|
}
|
||||||
|
return a.name > b.name ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChannelList() {
|
||||||
|
const { loading, error, data } = useQuery(gql`
|
||||||
|
{
|
||||||
|
workspace {
|
||||||
|
icon
|
||||||
|
name
|
||||||
|
channels {
|
||||||
|
name
|
||||||
|
isPrivate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const [gotoChat] = useMutation(gql`
|
||||||
|
mutation {
|
||||||
|
gotoChat(workspace: $workspace, channel: $channel) @client
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div>Loading</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div>{error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{data.workspace.map((ws: GQLWorkspace) => (
|
||||||
|
<article key={ws.name} className="workspace">
|
||||||
|
<div className="title">
|
||||||
|
<img src={ws.icon}></img> {ws.name}
|
||||||
|
</div>
|
||||||
|
<div className="chats">
|
||||||
|
<ul>
|
||||||
|
{ws.channels.sort(channelsFirst).map(ch => (
|
||||||
|
<li
|
||||||
|
key={ch.name}
|
||||||
|
onClick={a =>
|
||||||
|
gotoChat({
|
||||||
|
variables: { workspace: ws.name, channel: ch.name }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{ch.name}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
80
frontend/src/Chatroom.tsx
Normal file
80
frontend/src/Chatroom.tsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useQuery } from "@apollo/react-hooks";
|
||||||
|
import { gql } from "apollo-boost";
|
||||||
|
import { GQLMessage } from "./gql-types";
|
||||||
|
|
||||||
|
export default function Chatroom() {
|
||||||
|
const {
|
||||||
|
loading: localLoading,
|
||||||
|
error: localError,
|
||||||
|
data: localData
|
||||||
|
} = useQuery(gql`
|
||||||
|
{
|
||||||
|
currentChat {
|
||||||
|
workspace
|
||||||
|
channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const variables = {
|
||||||
|
workspace: localData.currentChat?.workspace || "",
|
||||||
|
channel: localData.currentChat?.channel || ""
|
||||||
|
};
|
||||||
|
const {
|
||||||
|
loading: remoteLoading,
|
||||||
|
error: remoteError,
|
||||||
|
data: remoteData
|
||||||
|
} = useQuery(
|
||||||
|
gql`
|
||||||
|
query Messages($workspace: String!, $channel: String!) {
|
||||||
|
messages(
|
||||||
|
workspace: $workspace
|
||||||
|
order: DATE_ASC
|
||||||
|
filter: { channel: $channel }
|
||||||
|
) {
|
||||||
|
messages {
|
||||||
|
content
|
||||||
|
username
|
||||||
|
userRealname
|
||||||
|
time
|
||||||
|
messageId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
variables
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (localLoading || remoteLoading) {
|
||||||
|
return <div>Loading</div>;
|
||||||
|
}
|
||||||
|
if (localData.currentChat == null) {
|
||||||
|
return <div>Select a channel from the left menu</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localError) {
|
||||||
|
return <div>{localError.message}</div>;
|
||||||
|
}
|
||||||
|
if (remoteError) {
|
||||||
|
return <div>{remoteError.message}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ul className="chatlog">
|
||||||
|
{remoteData.messages.messages.map((msg: GQLMessage) =>
|
||||||
|
<li key={msg.messageId}>
|
||||||
|
<article className="message">
|
||||||
|
<header>
|
||||||
|
{msg.userRealname} ({msg.username}) ({msg.messageId})
|
||||||
|
</header>
|
||||||
|
<p>{msg.content}</p>
|
||||||
|
</article>
|
||||||
|
</li>;
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
43
frontend/src/gql-types.ts
Normal file
43
frontend/src/gql-types.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
export interface GQLMessageList {
|
||||||
|
messages: GQLMessage[];
|
||||||
|
next?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GQLMessage {
|
||||||
|
time: Date;
|
||||||
|
content: string;
|
||||||
|
username: string;
|
||||||
|
userRealname: string;
|
||||||
|
channelName: string;
|
||||||
|
messageId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GQLWorkspace {
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
channels: GQLChannel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GQLChannel {
|
||||||
|
name: string;
|
||||||
|
isPrivate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GQLPagination {
|
||||||
|
after?: string;
|
||||||
|
first?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GQLMessageFilter {
|
||||||
|
channel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum GQLSortOrder {
|
||||||
|
DateAsc,
|
||||||
|
DateDesc
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GQLChatInfo {
|
||||||
|
workspace: string;
|
||||||
|
channel: string;
|
||||||
|
}
|
12
frontend/style/layout.scss
Normal file
12
frontend/style/layout.scss
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
@mixin full {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
.main {
|
||||||
|
@include full;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@import "./sidebar.scss";
|
25
frontend/style/sidebar.scss
Normal file
25
frontend/style/sidebar.scss
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
.channelList {
|
||||||
|
.workspace {
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-height: 2em;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chats {
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding: 2pt 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
frontend/tsconfig.json
Normal file
6
frontend/tsconfig.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"jsx": "react"
|
||||||
|
}
|
||||||
|
}
|
5571
frontend/yarn.lock
Normal file
5571
frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
Reference in a new issue