Update <ChannelList /> after channel freeze/unfreeze with frozen applied filter - getstream-io

I'm trying to build a chat frontend using the stream react components with the return of a ChatPage component that has this structure.
<ChatConfig config={config}>
<Chat client={client} customStyles={options?.theme}>
<ChannelList
Preview={CustomChannelPreview}
filters={filters}
sort={sort}
/>
<Channel>
<Window>
<CustomChannelHeader/>
<MessageList/>
<MessageInput Input={CustomMessageInput}/>
</Window>
<Thread/>
</Channel>
</Chat>
</ChatConfig>
So there are the channel list and the channel window in the same page where the user con freeze or unfreeze a channel.
The FE of the react application is capable of getting the filters to apply from the window.location.search so that the applied filters let the user view frozen or unfrozen channels only (e.g. {"frozen":{"$eq":false}, .....}).
When a user freezes or unfreezes, is there a way to re-render the channel list after the channel update to match the updated channel state?
Is it possible to do that without reimplementing the whole filtering strategy in the onChannelUpdated function?
Thanks in advance.

The ChannelList component accepts a key prop. When the value of this prop changes, the component re-renders and matches the updated channel state.
In your case, you can keep track of the channel list key state:
const generateUniqueChars = () => {
// any generation strategy you want here
}
const [channelListKey, setChannelListKey] = useState(generateUniqueChars())
const updateChannelListKey = () => {
setChannelListKey(generateUniqueChars())
}
Then, you pass the key to the ChannelList component:
<ChannelList key={channelListKey} />
Using React context, or any means you prefer, you can pass the updateChannelListKey function to the component that freezes/unfreezes a channel. When the freeze/unfreeze is triggered, you call the update channel list function, which updates the channel list key state, and re-renders the channel list to match the new state of channels.

Related

Get stream, System message customisation stream-chat

community
I implemented stream-chat & stream-chat-react getStream
Hi, I want to show the system message on some events
For Ex: User A added User B to the chat (Here user A and User B is the name of both user but I don't want to send those message as it is because I want if user A changes their name to USER X then those previous messages also updated.) I want guidance on how I can achieve this.
Stream is allowing me to send system messages with the addMemberToChannel event but I am not able to find how I can use it for my specific case.
Expected Output:
For your case, you will avoid saving hardcoded data in the message's text property.
First, you create a system message on adding a user to a channel like this:
channel.addMembers([userId], {
text: 'added_users',
mentioned_users: [userId],
});
With the addMembers method on a channel object, you can add members to a channel and also pass a message object.
The message object accepts the message text and the mentioned_users properties.
You can use added_users or any text that you want to keep as a standard message for adding-members-system-message. You'll see why I use "added_users" in a second.
The Channel component renders system messages using the EventComponent. This component displays the text of the system message, with the date and some added styles.
You can create a custom event message component for your added_users message. This component can look like this:
import { EventComponent } from 'stream-chat-react';
function CustomEventMessage(props) {
const { message } = props;
const { text, mentioned_users, user } = message;
if (text === 'added_users') {
const message = user?.name + ' added ' + mentioned_users[0].name;
return (
<div className="str-chat__message--system">
<div className="str-chat__message--system__text">
<div className="str-chat__message--system__line"></div>
<p>{message}</p>
<div className="str-chat__message--system__line"></div>
</div>
</div>
);
}
// use the default event component
return <EventComponent {...props} />;
}
The CustomEventMessage component above accepts a message object prop which has the:
text: system message text
mentioned_users: mentioned users in the message
user: user who triggered the message
Next, you check if the text is added_users. If it is, then you provide a custom UI and message, which consists of the name of the user who triggered the message and the mentioned user (who was added to the channel)
I also used classnames from Stream's stylesheets so that I don't have to build mine.
Next, you add this component to the Channel component:
<Channel MessageSystem={CustomEventMessage}>
// ...
</Channel>
The message will now read as "Person X added Person Y" as seen in the image below:
If the text is not added_users, you pass the props to the default EventComponent.
Since you're saving the id of the mentioned user and not hardcoding the text ("Person A added Person B"), you will also get updated details about the users, even when they update their information.

How to pass one more parameter other than option from the intent showing the carousel to the intent handling the carousel?

I am using an intent to first present the carousel to the user.
When the user clicks on one of the options in the carousel, in the handler intent I get the key of the carousel item that the user selected.
Example of carousel intent,
app.intent('search', async (conv,params) => {
conv.ask(`Choose one item`,new Carousel({
title :`Search results`,
items : carouselItems,
}));
});
Example of the handler intent,
app.intent('handle_carousel', async (conv,params,option) => {
const key = parseInt(option);
});
However, along with the key of the option selected I also want to pass another integer from the carousel intent to the handler intent.
This other integer is different for each option. You can think of the other integer as an ID, it's unique for each option.
How can I achieve that?
You have a few approaches for passing additional data that should be associated with each key.
The first is, as you note in your answer, storing that mapping in a table that is stored as part of session data (either using conv.data or a Dialogflow context).
Another is to encode that data as part of the key that you include with each option, and then decode the key when you get it back.
So, for example, you could make the key a result of an encode function like
function encodeOptionKey( key, otherValue ){
return `${key}:${otherValue}`
}
and then decode it with a function such as
function decodeOptionKey( option ){
const [key,otherValue] = option.split(':');
return {
key,
otherValue
}
}
and call this from your handler with something like
app.intent('handle_carousel', async (conv,params,option) => {
const {key, otherValue} = decodeOptionKey( option );
// ...
});
I created a map of the keys of various carousel options and the corresponding parameter I wanted to pass, saved that map in conv.data.store, which is the conversation storage provided by actions-on-google. Then I used that map to get the parameter from the carousel key that was being passed to the handler intent.
For example in the carousel intent :
let map = {
keyofcarousel : option,
other_parameter : otherparam,
};
conv.data.store = map;
Then call conv.data.store in the handler intent.

How to use CellMeasurer correctly?

I have an interactive component inside "react-virtualized" List that acts on clicks. When the component is clicked, the cell transforms i.e. height changes.
My first version of the rowRenderer:
rowRenderer ({index, isScrolling, key, style}) {
let message = this.props.messages[index];
return <Message key={message.id} text={message.text} />
}
When the message is clicked, a text field appears. This changes the height. What however happens is that component renders over the next message.
This happens because the instance of Message is different in UI and in CellMeasurer as you can see:
<CellMeasurer
cellRenderer={
// Here we return instance 1
({ rowIndex, ...rest }) => this.rowRenderer({ index: rowIndex, ...rest })
}
columnCount={1}
rowCount={messages.length}
>
{({ getRowHeight, resetMeasurementForRow }) => {
this.resetMeasurementForRow = resetMeasurementForRow;
return <List
height={height}
overscanRowCount={50}
rowCount={messages.length}
rowHeight={getRowHeight}
rowRenderer={this.rowRenderer} // Here we create another instance
width={width}
ref={(ref)=>{
this.list = ref;
}}
/>
}}
</CellMeasurer>
The instance created by List will obviously contain correct state but CellMeasurer is not aware of this state.
I tested the following approach but I highly doubt that this is the correct way to do this? I simply cache the UI component instance myself:
rowRenderer ({index, isScrolling, key, style}) {
let message = this.props.messages[index];
if(!this.componentCache[index]) {
this.componentCache[index] = <Message key={message.id} text={message.text} />
}
return this.componentCache[index];
}
This fixes this problem but probably introduces many other issues. What is the correct way to do this?
(I'm aware that using Flux/Redux/global state could fix this but I'm wondering is there some fundamental react-virtualized feature/aspect that I'm missing here.)
Answer to this is actually in the documentation:
The current implementation of CellMeasurer creates cells on demand,
measures them, and then throws them away. Future versions of this
component may try to clone or in some other way share cells with their
parent Grid in order to improve performance. However until that
happens, be wary of using CellMeasurer to measure stateful components.
Since cells are just-in-time created for measuring purposes they will
only be measured with their default state. To avoid this issue for
now, use controlled props (instead of state) for cell rendering
behavior.
So there is no other solution than handling the state outside the component. In practice, I fixed it as follows:
rowRenderer ({index, isScrolling, key, style}) {
let message = this.props.messages[index];
let selected = message.id === store.getState().selectedMessage;
return <Message key={message.id} text={message.text} selected={selected} />
}

AngularJS 1.5 component $postLink and data binding

I am building an Angular 1.5 component that wraps Chosen list, which needs to be initialized by calling .chosen() on the jQuery element. I can do that using the $postLink lifecycle callback, and something like $('.chosen-select').chosen(), which works fine. However, I can anticipate someone using multiple instances of the component on the same page, so using a class selector would not necessarily get the component you want.
I tried using an id selector instead, by adding a prefix to whatever id someone assigns to the component in HTML. For example, I may use the component like <chosen-select id="roles"></chosen-select> and in the template if have <select id="cs-{{$ctrl.id}}"> (in the controller, I bind id: '#'). This all works as expected EXCEPT that in $postLink, the select element has been created (and other bindings, such as the one that lists options, resolved) but id is still "cs-{{$ctrl.id}}". At what point does that become "cs-roles" (which is what it is in the DOM when everything has been set up)? What is the best way to ensure that I am accessing the object that belongs to this component?
Here is the component code, which works:
template:
<select id="cs-{{$ctrl.id}}" class="chosen-select"
ng-options="(option.name || option) for option in $ctrl.options track by (option.id || option)"
ng-model="$ctrl.result"
>
</select>
component:
mymod.component('chosenSelect', {
templateUrl: 'shared/components/chosenSelectComponent.html',
controller: chosenSelectController,
bindings: {
id: '#',
options: '<',
config: '<?',
selected: '<?',
doChange: '&?'
}
});
function chosenSelectController() {
var vm = this;
vm.result = vm.selected || vm.options[0];
vm.$postLink = function() {
// would like to use ("#cscomp-" + vm.id) to make sure it is unique,
// but id doesn't seem to have been resolved yet in select element
$(".chosen-select")
.chosen(vm.config)
.on('change', function(evt, params) {
// parms.selected also holds result
vm.doChange({ value: vm.result });
});
};
}
I realized I could use a hierarchical selector to solve the problem. In the $postLink function, referencing $("#" + vm.id + " .chosen-select") does exactly what I want it to by narrowing the selection to only elements that are descendants of the element with the specified id.

dijit.Tree search and refresh

I can't seem to figure out how to search in a dijit.Tree, using a ItemFileWriteStore and a TreeStoreModel. Everything is declarative, I am using Dojo 1.7.1, here is what I have so far :
<input type="text" dojoType="dijit.form.TextBox" name="search_fruit" id="search_fruit" onclick="search_fruit();">
<!-- store -->
<div data-dojo-id="fruitsStore" data-dojo-type="dojo.data.ItemFileWriteStore" clearOnClose="true" urlPreventCache="true" data-dojo-props='url:"fruits_store.php"'></div>
<!-- model -->
<div data-dojo-id="fruitsModel" data-dojo-type="dijit.tree.TreeStoreModel" data-dojo-props="store:fruitsStore, query:{}"></div>
<!-- tree -->
<div id="fruitsTree" data-dojo-type="dijit.Tree"
data-dojo-props='"class":"container",
model:fruitsModel,
dndController:"dijit.tree.dndSource",
betweenThreshold:5,
persist:true'>
</div>
The json returned by fruits_store.php is like this :
{"identifier":"id",
"label":"name",
"items":[{"id":"OYAHQIBVbeORMfBNZXFGOHPdaRMNUdWEDRPASHSVDBSKALKIcBZQ","name":"Fruits","children":[{"id":"bSKSVDdRMRfEFNccfTZbWHSACWbLJZMTNHDVVcYGcTBDcIdKIfYQ","name":"Banana"},{"id":"JYDeLNIGPDBRMcfSTMeERZZEUUIOMNEYYcNCaCQbCMIWOMQdMEZA","name":"Citrus","children":[{"id":"KdDUfEDaKOQMFNJaYbSbAcAPFBBdLALFMIPTFaYSeCaDOFaEPbJQ","name":"Orange"},{"id":"SDWbXWbTWKNJDIfdAdJbbbRWcLZFJHdEWASYDCeFOZYdcZUXJEUQ","name":"Lemon"}]},{"id":"fUdQTEZaIeBIWCHMeBZbPdEWWIQBFbVDbNFfJXNILYeBLbWUFYeQ","name":"Common ","children":[{"id":"MBeIUKReBHbFWPDFACFGWPePcNANPVdQLBBXYaTPRXXcTYRTJLDQ","name":"Apple"}]}]}]}
Using a grid instead of a tree, my search_fruit() function would look like this :
function search_fruit() {
var grid = dijit.byId('grid_fruits');
grid.query.search_txt = dijit.byId('search_fruit').get('value');
grid.selection.clear();
grid.store.close();
grid._refresh();
}
How to achieve the same using the tree ? Thanks !
The refreshing of a dijit.Tree becomes a little more complicated, since there is a model involved (which in grid afaik is inbuilt, the grid component implements query functionality)
Performing search via store
But how to search, thats incredibly easy whilst using the ItemFileReadStore. Syntax is as such:
myTree.model.store.fetch({
query: {
name: 'Oranges'
},
onComplete: function(items) {
dojo.forEach(items, function(item) {
console.log(myTree.model.store.getValue(item, "ID"));
});
}
});
Displaying search results only
As shown above, the store will fetch, the full payload is put into its _allItemsArray and the store queryengine then filters out what its told by query argument to the fetch method. At any time, we could call fetch on store, even without sending an XHR for json contents - fetch with query argument can be considered as a simple filter.
It becomes slightly more interesting to let the Model know about this query.. If you do so, it will only create treeNodes to fill the tree, based on the returned results from store.fetch({query:model.query});
So, instead of sending store.fetch with a callback, lets _try to set model query and update the tree.
// seing as we are working with a multi-parent tree model (ForestTree), the query Must match a toplevel item or else nothing is shown
myTree.model.query = { name:'Fruits' };
// below method must be implemented to do so runtime
// and note, that the DnD might become invalid
myTree.update();
Refreshing tree with new xhr-request from store
You need to do exactly as you do with regards to the store. Close it but then rebuild the model. Model contains all the TreeNodes (beneath its root-node) and the Tree itself maps an itemarray which needs to be cleared to avoid memory leakage.
So, performing following steps will rebuild the tree - however this sample does not take in account, if you have DnD activated, the dndSource/dndContainer will still reference the old DOM and thereby 'keep-alive' the previous DOMNode hierachy (hidden ofc).
By telling the model that its rootNode is UNCHECKED, the children of it will be checked for changes. This in turn will produce the subhierachy once the tree has done its _load()
Close the store (So that the store will do a new fetch()).
this.model.store.clearOnClose = true;
this.model.store.close();
Completely delete every node from the dijit.Tree
delete this._itemNodesMap;
this._itemNodesMap = {};
this.rootNode.state = "UNCHECKED";
delete this.model.root.children;
this.model.root.children = null;
Destroy the widget
this.rootNode.destroyRecursive();
Recreate the model, (with the model again)
this.model.constructor(this.model)
Rebuild the tree
this.postMixInProperties();
this._load();
Creds; All together as such, scoped onto the dijit.Tree:
new dijit.Tree({
// arguments
...
// And additional functionality
update : function() {
this.model.store.clearOnClose = true;
this.model.store.close();
delete this._itemNodesMap;
this._itemNodesMap = {};
this.rootNode.state = "UNCHECKED";
delete this.model.root.children;
this.model.root.children = null;
this.rootNode.destroyRecursive();
this.model.constructor(this.model)
this.postMixInProperties();
this._load();
}
});

Resources