Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Mattermost Plugin): Subscriptions #10932

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
"husky": "^7.0.4",
"jscodeshift": "^0.14.0",
"kysely": "^0.27.5",
"kysely-codegen": "^0.15.0",
"kysely-codegen": "^0.17.0",
"kysely-ctl": "^0.11.0",
"lerna": "^6.4.1",
"mini-css-extract-plugin": "^2.7.2",
Expand Down
116 changes: 110 additions & 6 deletions packages/mattermost-plugin/Atmosphere.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Variables} from 'react-relay'
import {
Environment,
GraphQLResponse,
Network,
Observable,
RecordSource,
Expand All @@ -9,21 +10,39 @@ import {
RequestParameters
} from 'relay-runtime'
import RelayModernStore from 'relay-runtime/lib/store/RelayModernStore'
import {v4 as uuid} from 'uuid'

import {AnyAction, Store} from '@reduxjs/toolkit'
import {Client4} from 'mattermost-redux/client'
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'
import {GlobalState} from 'mattermost-redux/types/store'
import {Sink} from 'relay-runtime/lib/network/RelayObservable'
import {login as onLogin} from './reducers'
import {authToken as getAuthToken} from './selectors'
RelayFeatureFlags.ENABLE_RELAY_RESOLVERS = true

type State = {
id: number
connectionId: string
serverUrl: string
store: Store<GlobalState, AnyAction>
subscriptions: Record<
string,
{
sink: Sink<any>
}
>
}

const decode = (msg: string) => {
const parsedData = JSON.parse(msg)
if (!Array.isArray(parsedData)) return {message: parsedData}
const [message, mid] = parsedData
return {message, mid}
}

const fetchGraphQL = (state: State) => (params: RequestParameters, variables: Variables) => {
const {serverUrl, store} = state
const {serverUrl, store, connectionId, id} = state
const authToken = getAuthToken(store.getState())
const response = fetch(
serverUrl + '/graphql',
Expand All @@ -32,9 +51,11 @@ const fetchGraphQL = (state: State) => (params: RequestParameters, variables: Va
headers: {
accept: 'application/json',
'content-type': 'application/json',
'x-application-authorization': authToken ? `Bearer ${authToken}` : ''
'x-application-authorization': authToken ? `Bearer ${authToken}` : '',
'x-correlation-id': connectionId || ''
},
body: JSON.stringify({
id,
type: 'start',
payload: {
documentId: params.id,
Expand All @@ -44,6 +65,7 @@ const fetchGraphQL = (state: State) => (params: RequestParameters, variables: Va
})
})
)
++state.id

return Observable.from(
response.then(async (data) => {
Expand All @@ -53,14 +75,70 @@ const fetchGraphQL = (state: State) => (params: RequestParameters, variables: Va
)
}

const subscribeGraphQL = (state: State) => (request: RequestParameters, variables: Variables) => {
return Observable.create<GraphQLResponse>((sink) => {
/*
const _next = sink.next
sink.next = (value: any) => {
const {data} = value
const subscriptionName = data ? Object.keys(data)[0] : undefined
const nullObj = this.subscriptionInterfaces[subscriptionName!]
const nextObj =
nullObj && subscriptionName
? {
...value,
data: {
...data,
[subscriptionName]: {
...nullObj,
...data[subscriptionName]
}
}
}
: value
_next(value)
}
*/

const {serverUrl, store, connectionId, id, subscriptions} = state
const authToken = getAuthToken(store.getState())
fetch(
serverUrl + '/graphql',
Client4.getOptions({
method: 'POST',
headers: {
accept: 'application/json',
'content-type': 'application/json',
'x-application-authorization': authToken ? `Bearer ${authToken}` : '',
'x-correlation-id': connectionId || ''
},
body: JSON.stringify({
id,
type: 'start',
payload: {
documentId: request.id,
query: request.text,
variables
}
})
})
)
subscriptions[id] = {
sink
}
++state.id
})
}

const login = (state: State) => async () => {
const {serverUrl, store} = state
const {serverUrl, store, connectionId} = state
const response = await fetch(
serverUrl + '/login',
Client4.getOptions({
method: 'POST',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'x-correlation-id': connectionId || ''
}
})
)
Expand All @@ -85,13 +163,19 @@ export class Atmosphere extends Environment {
login: () => Promise<void>

constructor(serverUrl: string, reduxStore: Store<GlobalState, AnyAction>) {
const currentUser = getCurrentUser(reduxStore.getState())
const {id} = currentUser

const state = {
id: 1,
connectionId: `${id}/${uuid()}`,
serverUrl,
store: reduxStore,
authToken: null
authToken: null,
subscriptions: {}
}

const network = Network.create(fetchGraphQL(state))
const network = Network.create(fetchGraphQL(state), subscribeGraphQL(state))
const relayStore = new RelayModernStore(new RecordSource(), {
resolverContext: {
store: reduxStore,
Expand All @@ -107,6 +191,26 @@ export class Atmosphere extends Environment {
// bind it here to avoid this == undefined errors
this.login = login(state)
}

onMessage(data: string) {
const {message} = decode(data)
const {id, type, payload} = message
const {subscriptions} = this.state
const subscription = subscriptions[id]
if (!subscription) return
const {sink} = subscription
switch (type) {
case 'data':
sink.next(payload)
break
case 'error':
sink.error(payload)
break
case 'complete':
sink.complete()
break
}
}
}

/**
Expand Down
42 changes: 42 additions & 0 deletions packages/mattermost-plugin/components/Sidepanel/SidePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import graphql from 'babel-plugin-relay/macro'
import {useState} from 'react'
import {useDispatch} from 'react-redux'
import {useSubscription} from 'react-relay'
import ReactSelect from 'react-select'
import {SidePanel_teamSubscription} from '../../__generated__/SidePanel_teamSubscription.graphql'
import {openLinkTeamModal, openStartActivityModal} from '../../reducers'
import ActiveMeetings from './ActiveMeetings'
import LinkedTeams from './LinkedTeams'
Expand All @@ -21,6 +24,45 @@ const panels = {
} as const

const SidePanel = () => {
useSubscription<SidePanel_teamSubscription>({
subscription: graphql`
subscription SidePanel_teamSubscription {
teamSubscription {
fieldName
StartCheckInSuccess {
...useStartMeeting_checkIn @relay(mask: false)
}
StartRetrospectiveSuccess {
...useStartMeeting_retrospective @relay(mask: false)
}
StartSprintPokerSuccess {
...useStartMeeting_sprintPoker @relay(mask: false)
}
StartTeamPromptSuccess {
...useStartMeeting_teamPrompt @relay(mask: false)
}
}
}
`,
variables: {}
/*
probably not needed if the subscription requests enough data
onNext: (data) => {
if (!data?.teamSubscription) return
switch (data.teamSubscription.fieldName) {
case 'StartCheckInSuccess':
break
case 'StartRetrospectiveSuccess':
break
case 'StartSprintPokerSuccess':
break
case 'StartTeamPromptSuccess':
break
}
}
*/
})

const [activePanel, setActivePanel] = useState<keyof typeof panels>('teams')
const dispatch = useDispatch()

Expand Down
46 changes: 36 additions & 10 deletions packages/mattermost-plugin/hooks/useStartMeeting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,38 @@ import {useStartMeetingSprintPokerMutation} from '../__generated__/useStartMeeti
import {useStartMeetingTeamPromptMutation} from '../__generated__/useStartMeetingTeamPromptMutation.graphql'
//import addNodeToArray from '../../client/utils/relay/addNodeToArray'

graphql`
fragment useStartMeeting_retrospective on StartRetrospectiveSuccess {
meeting {
id
}
}
`

graphql`
fragment useStartMeeting_checkIn on StartCheckInSuccess {
meeting {
id
}
}
`

graphql`
fragment useStartMeeting_sprintPoker on StartSprintPokerSuccess {
meeting {
id
}
}
`

graphql`
fragment useStartMeeting_teamPrompt on StartTeamPromptSuccess {
meeting {
id
}
}
`

const useStartMeeting = () => {
const [error, setError] = useState<Error | null>(null)
const [startRetrospective, startRetrospectiveLoading] =
Expand All @@ -19,6 +51,7 @@ const useStartMeeting = () => {
}
}
startRetrospective(teamId: $teamId) {
...useStartMeeting_retrospective @relay(mask: false)
... on ErrorPayload {
error {
message
Expand All @@ -31,6 +64,7 @@ const useStartMeeting = () => {
const [startCheckIn, startCheckInLoading] = useMutation<useStartMeetingCheckInMutation>(graphql`
mutation useStartMeetingCheckInMutation($teamId: ID!) {
startCheckIn(teamId: $teamId) {
...useStartMeeting_checkIn @relay(mask: false)
... on ErrorPayload {
error {
message
Expand All @@ -54,16 +88,12 @@ const useStartMeeting = () => {
}
}
startSprintPoker(teamId: $teamId) {
...useStartMeeting_sprintPoker @relay(mask: false)
... on ErrorPayload {
error {
message
}
}
... on StartSprintPokerSuccess {
meeting {
id
}
}
}
}
`)
Expand All @@ -72,16 +102,12 @@ const useStartMeeting = () => {
graphql`
mutation useStartMeetingTeamPromptMutation($teamId: ID!) {
startTeamPrompt(teamId: $teamId) {
...useStartMeeting_teamPrompt @relay(mask: false)
... on ErrorPayload {
error {
message
}
}
... on StartTeamPromptSuccess {
meeting {
id
}
}
}
}
`
Expand Down
3 changes: 3 additions & 0 deletions packages/mattermost-plugin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import commands from './public/mattermost-plugin-commands.json'
export const init = async (registry: PluginRegistry, store: Store<GlobalState, AnyAction>) => {
const serverUrl = getPluginServerRoute(store.getState())
const environment = createEnvironment(serverUrl, store)
registry.registerWebSocketEventHandler(`custom_${manifest.id}_graphql`, (data) => {
environment.onMessage(data.data.payload)
})
registry.registerSlashCommandWillBePostedHook(async (message: string, args: ContextArgs) => {
const [command, subcommand] = message.split(/\s+/)
if (command === '/parabol') {
Expand Down
Loading
Loading