Compare commits

..
Author SHA1 Message Date
deepend b0a5c6f5e5 Improve GTK3 Performance 2026-09-09 16:45:28 -06:00
deepend bb3893d36c Fix channel ACTION routing after self-message handling 2026-09-03 09:27:07 -06:00
deepend-tildeclub fe57449f6c Merge pull request #374 from TehPeGaSuS/fix/weechat-relay-pm-split
Fix private messages splitting into two windows over WeeChat's IRC relay
2026-09-03 08:20:55 -07:00
ThePeGaSuS c725adaa09 Fix CTCP ACTION not rerouting self-sent /me to correct PM window
Mirrors the PRIVMSG fix: replayed /me messages sent by our own nick
via WeeChat relay backlog were always passed with fromme=FALSE,
so they never got rerouted to the actual PM query window.
2026-09-03 08:51:21 +02:00
ThePeGaSuS 0ba523d071 Fix: private messages split into two windows over WeeChat's IRC relay
When you send a private message, ZoiteChat sometimes gets the same
message echoed back by the server so it can show it in the right
window. Whether it recognizes that echo depended on the server first
confirming a feature called "echo-message" during connection setup.

WeeChat's relay doesn't seem to confirm that feature the way other
servers (or ZNC) do, so ZoiteChat didn't recognize its own echoed
messages and treated them as brand new incoming messages from
yourself, opening a second, separate conversation window instead of
using the one you were already typing in.

This makes ZoiteChat recognize its own message by checking who sent
it, instead of only trusting that feature confirmation. That way,
sent and received messages always end up in the same conversation
window, regardless of how well the server/relay announces its
capabilities.
2026-08-28 20:48:12 +02:00
16 changed files with 56 additions and 228 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
libxkbcommon0 \ libxkbcommon0 \
libgtk-3-bin libglib2.0-bin shared-mime-info gsettings-desktop-schemas \ libgtk-3-bin libglib2.0-bin shared-mime-info gsettings-desktop-schemas \
liblua5.4-dev libpci-dev libperl-dev libssl-dev libayatana-appindicator3-dev \ liblua5.4-dev libpci-dev libperl-dev libssl-dev libayatana-appindicator3-dev \
perl python3 python3-minimal python3-dev python3-cffi python3-sphinx mono-devel desktop-file-utils \ perl python3 python3-minimal python3-dev python3-cffi mono-devel desktop-file-utils \
fonts-noto-color-emoji breeze-gtk-theme \ fonts-noto-color-emoji breeze-gtk-theme \
patchelf file curl patchelf file curl
-1
View File
@@ -40,7 +40,6 @@ jobs:
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install cffi python -m pip install cffi
python -m pip install zstandard python -m pip install zstandard
python -m pip install sphinx
$ProgressPreference = 'SilentlyContinue' $ProgressPreference = 'SilentlyContinue'
function Download-WithRetry { function Download-WithRetry {
-42
View File
@@ -1,42 +0,0 @@
#!/usr/bin/env python3
import os
import shutil
import subprocess
import sys
def main():
if len(sys.argv) != 4:
raise SystemExit("usage: build-docs.py SOURCE_DIR OUTPUT_DIR STAMP_FILE")
source_dir = os.path.abspath(sys.argv[1])
output_dir = os.path.abspath(sys.argv[2])
stamp_file = os.path.abspath(sys.argv[3])
doctree_dir = output_dir + ".doctrees"
shutil.rmtree(output_dir, ignore_errors=True)
shutil.rmtree(doctree_dir, ignore_errors=True)
os.makedirs(output_dir, exist_ok=True)
subprocess.run(
[
sys.executable,
"-m",
"sphinx",
"-b",
"html",
"-d",
doctree_dir,
source_dir,
output_dir,
],
check=True,
)
with open(stamp_file, "w", encoding="utf-8") as stamp:
stamp.write("built\n")
if __name__ == "__main__":
main()
-36
View File
@@ -1,36 +0,0 @@
#!/usr/bin/env python3
import os
import shutil
import sys
def main():
if len(sys.argv) != 4:
raise SystemExit("usage: install-docs.py STAMP_FILE SOURCE_DIR DATADIR")
_, source_dir, datadir = sys.argv[1:]
source_dir = os.path.abspath(source_dir)
if not os.path.isfile(sys.argv[1]):
raise SystemExit("documentation build did not produce its stamp file")
if not os.path.isfile(os.path.join(source_dir, "index.html")):
raise SystemExit("documentation build did not produce index.html")
if os.path.isabs(datadir):
destdir = os.environ.get("DESTDIR", "")
if destdir:
data_root = os.path.join(destdir, datadir.lstrip("/\\"))
else:
data_root = datadir
else:
data_root = os.path.join(os.environ["MESON_INSTALL_DESTDIR_PREFIX"], datadir)
destination = os.path.join(data_root, "doc", "zoitechat", "html")
shutil.rmtree(destination, ignore_errors=True)
os.makedirs(os.path.dirname(destination), exist_ok=True)
shutil.copytree(source_dir, destination)
if __name__ == "__main__":
main()
-1
View File
@@ -54,7 +54,6 @@
"buildsystem": "meson", "buildsystem": "meson",
"config-opts": [ "config-opts": [
"-Ddbus-service-use-appid=true", "-Ddbus-service-use-appid=true",
"-Dinstall-docs=false",
"-Dwith-perl=perl", "-Dwith-perl=perl",
"-Dwith-python=python3", "-Dwith-python=python3",
"-Dwith-lua=lua" "-Dwith-lua=lua"
-38
View File
@@ -35,9 +35,6 @@ config_h.set_quoted('PACKAGE_NAME', meson.project_name())
config_h.set_quoted('GETTEXT_PACKAGE', 'zoitechat') config_h.set_quoted('GETTEXT_PACKAGE', 'zoitechat')
config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'),
get_option('datadir'), 'locale')) get_option('datadir'), 'locale'))
config_h.set_quoted('ZOITECHAT_DOCDIR',
join_paths(get_option('prefix'), get_option('datadir'), 'doc', 'zoitechat', 'html')
)
config_h.set10('ENABLE_NLS', true) config_h.set10('ENABLE_NLS', true)
# Optional features # Optional features
@@ -163,41 +160,6 @@ foreach ldflag : test_ldflags
endforeach endforeach
add_project_link_arguments(global_ldflags, language: 'c') add_project_link_arguments(global_ldflags, language: 'c')
if get_option('install-docs')
docs_python = find_program('python3', 'python')
docs_sphinx = run_command(docs_python, '-c', 'import sphinx', check: false)
if docs_sphinx.returncode() != 0
error('Sphinx is required to build the local HTML documentation. Install Sphinx or configure with -Dinstall-docs=false.')
endif
docs_source_dir = join_paths(meson.source_root(), 'docs')
docs_output_dir = join_paths(meson.build_root(), 'docs-html')
docs_build_script = join_paths(docs_source_dir, 'build-docs.py')
docs_install_script = join_paths(docs_source_dir, 'install-docs.py')
docs_target = custom_target(
'html-documentation',
output: 'html-documentation.stamp',
command: [
docs_python,
docs_build_script,
docs_source_dir,
docs_output_dir,
'@OUTPUT@',
],
build_by_default: true,
build_always_stale: true,
)
meson.add_install_script(
docs_python,
docs_install_script,
docs_target,
docs_output_dir,
get_option('datadir'),
)
endif
subdir('src') subdir('src')
if get_option('plugin') if get_option('plugin')
subdir('plugins') subdir('plugins')
-3
View File
@@ -33,9 +33,6 @@ option('install-appdata', type: 'boolean',
option('install-plugin-metainfo', type: 'boolean', value: false, option('install-plugin-metainfo', type: 'boolean', value: false,
description: 'Installs metainfo files for enabled plugins, useful when distros create split packages' description: 'Installs metainfo files for enabled plugins, useful when distros create split packages'
) )
option('install-docs', type: 'boolean', value: true,
description: 'Build and install the local HTML documentation'
)
# Plugins # Plugins
option('with-checksum', type: 'boolean', option('with-checksum', type: 'boolean',
-2
View File
@@ -15,7 +15,6 @@ BuildRequires: perl
BuildRequires: perl-devel BuildRequires: perl-devel
BuildRequires: python3 BuildRequires: python3
BuildRequires: python3-cffi BuildRequires: python3-cffi
BuildRequires: python3-sphinx
BuildRequires: publicsuffix-list BuildRequires: publicsuffix-list
BuildRequires: xwayland-run BuildRequires: xwayland-run
BuildRequires: weston BuildRequires: weston
@@ -88,7 +87,6 @@ xwfb-run -- /usr/bin/meson test -C %{_vpath_builddir} --num-processes %{_smp_bui
%{_datadir}/icons/hicolor/scalable/apps/net.zoite.Zoitechat.svg %{_datadir}/icons/hicolor/scalable/apps/net.zoite.Zoitechat.svg
%{_datadir}/metainfo/net.zoite.Zoitechat.appdata.xml %{_datadir}/metainfo/net.zoite.Zoitechat.appdata.xml
%{_datadir}/metainfo/net.zoite.Zoitechat*.metainfo.xml %{_datadir}/metainfo/net.zoite.Zoitechat*.metainfo.xml
%{_datadir}/doc/zoitechat/html/
%dir %{_libdir}/zoitechat %dir %{_libdir}/zoitechat
%dir %{_libdir}/zoitechat/plugins %dir %{_libdir}/zoitechat/plugins
%dir %{_libdir}/zoitechat/python %dir %{_libdir}/zoitechat/python
+15 -1
View File
@@ -126,7 +126,21 @@ ctcp_handle (session *sess, char *to, char *nick, char *ip,
if (ctcp_check (sess, nick, word, word_eol, word[4] + ctcp_offset)) if (ctcp_check (sess, nick, word, word_eol, word[4] + ctcp_offset))
goto generic; goto generic;
inbound_action (sess, to, nick, ip, msg + 7, FALSE, tags_data->identified, tags_data); {
gboolean private_fromme = !serv->p_cmp (nick, serv->nick) && !is_channel (serv, to);
if (private_fromme)
{
session *target = find_dialog (serv, to);
if (target)
sess = target;
else if (serv->front_session)
sess = serv->front_session;
}
inbound_action (sess, to, nick, ip, msg + 7, private_fromme, tags_data->identified, tags_data);
}
return; return;
} }
+1 -1
View File
@@ -1368,7 +1368,7 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[],
{ {
if (ignore_check (word[1], IG_PRIV)) if (ignore_check (word[1], IG_PRIV))
return; return;
if (serv->have_echo_message && !serv->p_cmp (nick, serv->nick)) if (!serv->p_cmp (nick, serv->nick))
{ {
session *target_sess = find_dialog (serv, to); session *target_sess = find_dialog (serv, to);
+1 -1
View File
@@ -114,7 +114,7 @@ typedef struct restore_gui
/* information stored when this tab isn't front-most */ /* information stored when this tab isn't front-most */
GtkListStore *user_model; /* for filling the GtkTreeView */ GtkListStore *user_model; /* for filling the GtkTreeView */
GHashTable *user_row_refs; GHashTable *user_row_iters; /* User * -> persistent GtkTreeIter * */
void *buffer; /* xtext_Buffer */ void *buffer; /* xtext_Buffer */
char *input_text; /* input text buffer (while not-front tab) */ char *input_text; /* input text buffer (while not-front tab) */
char *topic_text; /* topic GtkEntry buffer */ char *topic_text; /* topic GtkEntry buffer */
+2 -2
View File
@@ -5786,9 +5786,9 @@ void
fe_session_callback (session *sess) fe_session_callback (session *sess)
{ {
gtk_xtext_buffer_free (sess->res->buffer); gtk_xtext_buffer_free (sess->res->buffer);
if (sess->res->user_row_iters)
g_hash_table_destroy (sess->res->user_row_iters);
g_object_unref (G_OBJECT (sess->res->user_model)); g_object_unref (G_OBJECT (sess->res->user_model));
if (sess->res->user_row_refs)
g_hash_table_destroy (sess->res->user_row_refs);
if (sess->res->banlist && sess->res->banlist->window) if (sess->res->banlist && sess->res->banlist->window)
mg_close_gen (NULL, sess->res->banlist->window); mg_close_gen (NULL, sess->res->banlist->window);
-41
View File
@@ -1740,50 +1740,9 @@ menu_ctcpguiopen (void)
editlist_gui_open (NULL, NULL, ctcp_list, buf, "ctcpreply", "ctcpreply.conf", ctcp_help); editlist_gui_open (NULL, NULL, ctcp_list, buf, "ctcpreply", "ctcpreply.conf", ctcp_help);
} }
static char *
menu_find_local_docs (void)
{
char *path;
#ifdef WIN32
char *base_path;
base_path = g_win32_get_package_installation_directory_of_module (NULL);
if (!base_path)
return NULL;
path = g_build_filename (base_path, "share", "doc", "zoitechat", "html", "index.html", NULL);
g_free (base_path);
#else
const char *appdir;
appdir = g_getenv ("APPDIR");
if (appdir && *appdir)
path = g_build_filename (appdir, "usr", "share", "doc", "zoitechat", "html", "index.html", NULL);
else
path = g_build_filename (ZOITECHAT_DOCDIR, "index.html", NULL);
#endif
if (g_file_test (path, G_FILE_TEST_IS_REGULAR))
return path;
g_free (path);
return NULL;
}
static void static void
menu_docs (GtkWidget *wid, gpointer none) menu_docs (GtkWidget *wid, gpointer none)
{ {
char *path;
path = menu_find_local_docs ();
if (path)
{
fe_open_url (path);
g_free (path);
return;
}
fe_open_url ("https://docs.zoitechat.org/en/latest/"); fe_open_url ("https://docs.zoitechat.org/en/latest/");
} }
+20 -47
View File
@@ -218,75 +218,49 @@ scroll_to_iter (GtkTreeIter *iter, GtkTreeView *treeview, GtkTreeModel *model)
static GHashTable * static GHashTable *
userlist_row_map_ensure (session *sess) userlist_row_map_ensure (session *sess)
{ {
if (!sess->res->user_row_refs) if (!sess->res->user_row_iters)
sess->res->user_row_refs = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) gtk_tree_row_reference_free); sess->res->user_row_iters = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) gtk_tree_iter_free);
return sess->res->user_row_refs; return sess->res->user_row_iters;
} }
static void static void
userlist_row_map_remove (session *sess, struct User *user) userlist_row_map_remove (session *sess, struct User *user)
{ {
if (!sess->res->user_row_refs) if (!sess->res->user_row_iters)
return; return;
g_hash_table_remove (sess->res->user_row_refs, user); g_hash_table_remove (sess->res->user_row_iters, user);
} }
static void static void
userlist_row_map_set (session *sess, GtkTreeModel *model, struct User *user, GtkTreeIter *iter) userlist_row_map_set (session *sess, GtkTreeModel *model, struct User *user, GtkTreeIter *iter)
{ {
GtkTreePath *path; /* The shared tree view can still show another session's model while a
GtkTreeRowReference *ref; * tab switch is pending. Never cache an iterator from that model. */
if (model != GTK_TREE_MODEL (sess->res->user_model))
path = gtk_tree_model_get_path (model, iter);
if (!path)
return; return;
ref = gtk_tree_row_reference_new (model, path); /* GtkListStore guarantees persistent iterators until their row is
gtk_tree_path_free (path); * removed, including across sorting. Unlike row references, these do
if (!ref) * not require every cached position to be updated on each insertion. */
return; g_hash_table_replace (userlist_row_map_ensure (sess), user, gtk_tree_iter_copy (iter));
g_hash_table_replace (userlist_row_map_ensure (sess), user, ref);
} }
static gboolean static gboolean
userlist_row_map_get_iter (session *sess, GtkTreeModel *model, struct User *user, GtkTreeIter *iter) userlist_row_map_get_iter (session *sess, GtkTreeModel *model, struct User *user, GtkTreeIter *iter)
{ {
GtkTreeRowReference *ref; GtkTreeIter *cached;
GtkTreePath *path;
struct User *row_user;
if (!sess->res->user_row_refs) if (model != GTK_TREE_MODEL (sess->res->user_model) || !sess->res->user_row_iters)
return FALSE; return FALSE;
ref = g_hash_table_lookup (sess->res->user_row_refs, user); cached = g_hash_table_lookup (sess->res->user_row_iters, user);
if (!ref) if (!cached)
return FALSE; return FALSE;
path = gtk_tree_row_reference_get_path (ref); /* Removal and clear invalidate the cache before deleting model rows. */
if (!path) *iter = *cached;
{
g_hash_table_remove (sess->res->user_row_refs, user);
return FALSE;
}
if (!gtk_tree_model_get_iter (model, iter, path))
{
gtk_tree_path_free (path);
g_hash_table_remove (sess->res->user_row_refs, user);
return FALSE;
}
gtk_tree_path_free (path);
gtk_tree_model_get (model, iter, COL_USER, &row_user, -1);
if (row_user != user)
{
g_hash_table_remove (sess->res->user_row_refs, user);
return FALSE;
}
return TRUE; return TRUE;
} }
@@ -587,7 +561,6 @@ fe_userlist_rehash (session *sess, struct User *user)
GTK_TREE_MODEL(sess->res->user_model), user, &sel); GTK_TREE_MODEL(sess->res->user_model), user, &sel);
if (!iter) if (!iter)
return; return;
userlist_row_map_set (sess, GTK_TREE_MODEL (sess->res->user_model), user, iter);
if (prefs.hex_away_track && user->away) if (prefs.hex_away_track && user->away)
{ {
@@ -698,8 +671,8 @@ fe_userlist_insert (session *sess, struct User *newuser, gboolean sel)
void void
fe_userlist_clear (session *sess) fe_userlist_clear (session *sess)
{ {
if (sess->res->user_row_refs) if (sess->res->user_row_iters)
g_hash_table_remove_all (sess->res->user_row_refs); g_hash_table_remove_all (sess->res->user_row_iters);
gtk_list_store_clear (sess->res->user_model); gtk_list_store_clear (sess->res->user_model);
} }
+16 -7
View File
@@ -3130,6 +3130,7 @@ gtk_xtext_class_init (GtkXTextClass * class)
typedef struct chunk_s { typedef struct chunk_s {
GSList *slp; GSList *slp;
gboolean collect_metadata;
int off1, len1, emph; int off1, len1, emph;
offlen_t meta; offlen_t meta;
} chunk_t; } chunk_t;
@@ -3142,12 +3143,19 @@ xtext_do_chunk(chunk_t *c)
if (c->len1 == 0) if (c->len1 == 0)
return; return;
/* Copying, searching and saving only need the stripped text. */
if (!c->collect_metadata)
{
c->len1 = 0;
return;
}
meta = g_new (offlen_t, 1); meta = g_new (offlen_t, 1);
meta->off = c->off1; meta->off = c->off1;
meta->len = c->len1; meta->len = c->len1;
meta->emph = c->emph; meta->emph = c->emph;
meta->width = 0; meta->width = 0;
c->slp = g_slist_append (c->slp, meta); c->slp = g_slist_prepend (c->slp, meta);
c->len1 = 0; c->len1 = 0;
} }
@@ -3170,6 +3178,7 @@ gtk_xtext_strip_color (unsigned char *text, int len, unsigned char *outbuf,
new_str = outbuf; new_str = outbuf;
c.slp = NULL; c.slp = NULL;
c.collect_metadata = slpp != NULL;
c.off1 = 0; c.off1 = 0;
c.len1 = 0; c.len1 = 0;
c.emph = 0; c.emph = 0;
@@ -3240,9 +3249,7 @@ bad_utf8: /* Normal ending sequence, and give up if bad utf8 */
*newlen = i; *newlen = i;
if (slpp) if (slpp)
*slpp = c.slp; *slpp = g_slist_reverse (c.slp);
else
g_slist_free_full (c.slp, g_free);
return new_str; return new_str;
} }
@@ -4341,7 +4348,7 @@ gtk_xtext_lines_taken (xtext_buffer *buf, textentry * ent)
if (win_width >= ent->indent + ent->str_width) if (win_width >= ent->indent + ent->str_width)
{ {
ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER (ent->str_len)); ent->sublines = g_slist_prepend (ent->sublines, GINT_TO_POINTER (ent->str_len));
ent->subline_count = 1; ent->subline_count = 1;
return ent->subline_count; return ent->subline_count;
} }
@@ -4352,13 +4359,15 @@ gtk_xtext_lines_taken (xtext_buffer *buf, textentry * ent)
do do
{ {
len = find_next_wrap (buf->xtext, ent, str, win_width, indent); len = find_next_wrap (buf->xtext, ent, str, win_width, indent);
ent->sublines = g_slist_append (ent->sublines, GINT_TO_POINTER (str + len - ent->str)); ent->sublines = g_slist_prepend (ent->sublines, GINT_TO_POINTER (str + len - ent->str));
ent->subline_count++;
indent = buf->indent; indent = buf->indent;
str += len; str += len;
} }
while (str < ent->str + ent->str_len); while (str < ent->str + ent->str_len);
ent->subline_count = g_slist_length (ent->sublines); /* Preserve display order without walking the growing list per wrap. */
ent->sublines = g_slist_reverse (ent->sublines);
return ent->subline_count; return ent->subline_count;
} }
-4
View File
@@ -30,8 +30,6 @@
</ItemGroup> </ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Target Name="Build"> <Target Name="Build">
<Exec Command="&quot;$(Python3Path)\python.exe&quot; &quot;$(SolutionDir)..\docs\build-docs.py&quot; &quot;$(SolutionDir)..\docs&quot; &quot;$(ZoiteChatBuild)\$(ZoiteChatPlatform)\docs-html&quot; &quot;$(ZoiteChatBuild)\$(ZoiteChatPlatform)\html-documentation.stamp&quot;" />
<ItemGroup> <ItemGroup>
<None Include="$(DepsRoot)\bin\*atk-1.0-0.dll" /> <None Include="$(DepsRoot)\bin\*atk-1.0-0.dll" />
<None Include="$(DepsRoot)\bin\*cairo*.dll" /> <None Include="$(DepsRoot)\bin\*cairo*.dll" />
@@ -84,7 +82,6 @@
<GdkPixbufLoaderCache Include="$(DepsRoot)\lib\gdk-pixbuf-2.0\**\loaders.cache" /> <GdkPixbufLoaderCache Include="$(DepsRoot)\lib\gdk-pixbuf-2.0\**\loaders.cache" />
<FontConfig Include="$(DepsRoot)\etc\fonts\*" /> <FontConfig Include="$(DepsRoot)\etc\fonts\*" />
<Docs Include="$(ZoiteChatBuild)\$(ZoiteChatPlatform)\docs-html\**\*" />
<Share Include="share\**\*" /> <Share Include="share\**\*" />
<Locale Include="$(ZoiteChatBin)locale\**\*;$(DepsRoot)\share\locale\**\*" /> <Locale Include="$(ZoiteChatBin)locale\**\*;$(DepsRoot)\share\locale\**\*" />
<MSWindowsTheme Include="$(DepsRoot)\share\themes\MS-Windows\**\*" /> <MSWindowsTheme Include="$(DepsRoot)\share\themes\MS-Windows\**\*" />
@@ -100,7 +97,6 @@
<Copy SourceFiles="@(GdkPixbufLoaderCache)" DestinationFiles="@(GdkPixbufLoaderCache->'$(ZoiteChatRel)\lib\gdk-pixbuf-2.0\%(RecursiveDir)%(Filename)%(Extension)')" /> <Copy SourceFiles="@(GdkPixbufLoaderCache)" DestinationFiles="@(GdkPixbufLoaderCache->'$(ZoiteChatRel)\lib\gdk-pixbuf-2.0\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(GSettingsSchemas)" DestinationFiles="@(GSettingsSchemas->'$(ZoiteChatRel)\share\glib-2.0\schemas\%(Filename)%(Extension)')" /> <Copy SourceFiles="@(GSettingsSchemas)" DestinationFiles="@(GSettingsSchemas->'$(ZoiteChatRel)\share\glib-2.0\schemas\%(Filename)%(Extension)')" />
<Copy SourceFiles="@(Share)" DestinationFiles="@(Share->'$(ZoiteChatRel)\share\%(RecursiveDir)%(Filename)%(Extension)')" /> <Copy SourceFiles="@(Share)" DestinationFiles="@(Share->'$(ZoiteChatRel)\share\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="@(Docs)" DestinationFiles="@(Docs->'$(ZoiteChatRel)\share\doc\zoitechat\html\%(RecursiveDir)%(Filename)%(Extension)')" />
<Copy SourceFiles="..\..\COPYING" DestinationFolder="$(ZoiteChatRel)\share\doc\zoitechat" /> <Copy SourceFiles="..\..\COPYING" DestinationFolder="$(ZoiteChatRel)\share\doc\zoitechat" />
<Copy SourceFiles="$(WinSparklePath)\COPYING" DestinationFolder="$(ZoiteChatRel)\share\doc\WinSparkle" /> <Copy SourceFiles="$(WinSparklePath)\COPYING" DestinationFolder="$(ZoiteChatRel)\share\doc\WinSparkle" />
<Copy SourceFiles="@(EnchantProviders)" DestinationFolder="$(ZoiteChatRel)\lib\enchant-2" /> <Copy SourceFiles="@(EnchantProviders)" DestinationFolder="$(ZoiteChatRel)\lib\enchant-2" />