Frontend works

This commit is contained in:
Hamcha 2020-01-27 12:56:06 +01:00
parent a572ca0d7c
commit 73bfff6a0f
Signed by: hamcha
GPG key ID: 44AD3571EB09A39E
13 changed files with 5942 additions and 13 deletions

View file

@ -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())
}, },
}) })
})?; })?;

View file

@ -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
View 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
View 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
View 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
View 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>
);
}

View 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
View 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
View 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;
}

View 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";

View 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
View file

@ -0,0 +1,6 @@
{
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react"
}
}

5571
frontend/yarn.lock Normal file

File diff suppressed because it is too large Load diff