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 {
|
||||
format!("#{}", userchname.unwrap_or("<unknown>".to_string()))
|
||||
} 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")]
|
||||
icon: String,
|
||||
|
||||
#[graphql(description = "List of channels and private chats")]
|
||||
channels: Vec<Channel>,
|
||||
}
|
||||
|
||||
#[derive(Debug, juniper::GraphQLObject)]
|
||||
|
@ -104,7 +107,12 @@ impl juniper::Context for Context {}
|
|||
|
||||
/// Get message id for slack message
|
||||
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
|
||||
|
@ -137,17 +145,6 @@ impl Query {
|
|||
"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>> {
|
||||
let dbs = context
|
||||
.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(
|
||||
context: &Context,
|
||||
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