Implemented readmarkers for threads; implemented LoadIdsByConditions within transaction; refactored actions; InsertOrUpdateChange now takes dataToInsert and DataToUpdate; fixed a deadlock in InsertOrUpdateChange

main
Inga 🏳‍🌈 15 years ago
parent 24686a37af
commit a314b252cf
  1. 2
      Builder/IISMainHandler/build.txt
  2. 2
      Builder/IISUploadHandler/build.txt
  3. 7
      Common/actions/AbstractChange.cs
  4. 15
      Common/actions/ChangeSet.cs
  5. 5
      Common/actions/InsertChange.cs
  6. 22
      Common/actions/InsertOrUpdateChange.cs
  7. 4
      Common/actions/UpdateChange.cs
  8. 5
      Common/dataobjects/Post.cs
  9. 143
      Common/dataobjects/Thread.cs
  10. 2
      Core/DB/IDBConnection.cs
  11. 4
      Core/DB/conditions/ComplexCondition.cs
  12. 2
      IISMainHandler/handlers/BoardHandler.cs
  13. 10
      IISMainHandler/handlers/PostHandler.cs
  14. 17
      IISMainHandler/handlers/ThreadHandler.cs
  15. 23
      MySQLConnector/Connection.cs
  16. 13
      templates/Full/elems/PostInfo.xslt
  17. 12
      templates/Full/elems/ThreadInfo.xslt

@ -26,12 +26,9 @@ namespace FLocal.Common.actions {
public readonly ISqlObjectTableSpec tableSpec; public readonly ISqlObjectTableSpec tableSpec;
protected readonly Dictionary<string, AbstractFieldValue> data; protected AbstractChange(ISqlObjectTableSpec tableSpec, IEnumerable<AbstractFieldValue> data) {
protected AbstractChange(ISqlObjectTableSpec tableSpec, Dictionary<string, AbstractFieldValue> data) {
this.tableSpec = tableSpec; this.tableSpec = tableSpec;
this.data = data; this.references = from val in data where val is ReferenceFieldValue select ((ReferenceFieldValue)val).referenced;
this.references = from kvp in data where kvp.Value is ReferenceFieldValue select ((ReferenceFieldValue)kvp.Value).referenced;
this.isApplied = false; this.isApplied = false;
} }

@ -19,12 +19,13 @@ namespace FLocal.Common.actions {
return Cache<IEnumerable<string>>.instance.get( return Cache<IEnumerable<string>>.instance.get(
tablesLockOrder_locker, tablesLockOrder_locker,
() => new List<string>() { () => new List<string>() {
"Accounts", dataobjects.Account.TableSpec.TABLE,
"Users", dataobjects.User.TableSpec.TABLE,
"Boards", dataobjects.Board.TableSpec.TABLE,
"Threads", dataobjects.Thread.TableSpec.TABLE,
"Posts", dataobjects.Post.TableSpec.TABLE,
"Sessions", dataobjects.Thread.ReadMarkerTableSpec.TABLE,
dataobjects.Session.TableSpec.TABLE,
} }
); );
} }
@ -94,7 +95,9 @@ namespace FLocal.Common.actions {
//if(!this.isProcessed) throw new CriticalException("ChangeSet is not processed yet"); //if(!this.isProcessed) throw new CriticalException("ChangeSet is not processed yet");
foreach(KeyValuePair<string, HashSet<AbstractChange>> kvp in this.changesByTable) { foreach(KeyValuePair<string, HashSet<AbstractChange>> kvp in this.changesByTable) {
foreach(AbstractChange change in kvp.Value) { foreach(AbstractChange change in kvp.Value) {
if(change.getId().HasValue) {
change.tableSpec.refreshSqlObject(change.getId().Value); change.tableSpec.refreshSqlObject(change.getId().Value);
} //otherwise we're disposing because of sql error or something, so we should show real cause of problem, not "id is null"
} }
} }
} }

@ -9,9 +9,12 @@ namespace FLocal.Common.actions {
private int? id; private int? id;
private Dictionary<string, AbstractFieldValue> data;
public InsertChange(ISqlObjectTableSpec tableSpec, Dictionary<string, AbstractFieldValue> data) public InsertChange(ISqlObjectTableSpec tableSpec, Dictionary<string, AbstractFieldValue> data)
: base(tableSpec, data) { : base(tableSpec, from kvp in data select kvp.Value) {
this.id = null; this.id = null;
this.data = data;
} }
public override int? getId() { public override int? getId() {

@ -13,10 +13,15 @@ namespace FLocal.Common.actions {
private AbstractCondition condition; private AbstractCondition condition;
public InsertOrUpdateChange(ISqlObjectTableSpec tableSpec, Dictionary<string, AbstractFieldValue> data, AbstractCondition condition) private Dictionary<string, AbstractFieldValue> dataToInsert;
: base(tableSpec, data) { private Dictionary<string, AbstractFieldValue> dataToUpdate;
public InsertOrUpdateChange(ISqlObjectTableSpec tableSpec, Dictionary<string, AbstractFieldValue> dataToInsert, Dictionary<string, AbstractFieldValue> dataToUpdate, AbstractCondition condition)
: base(tableSpec, (from kvp in dataToInsert select kvp.Value).Union(from kvp in dataToUpdate select kvp.Value)) {
this.id = null; this.id = null;
this.condition = condition; this.condition = condition;
this.dataToInsert = dataToInsert;
this.dataToUpdate = dataToUpdate;
} }
public override int? getId() { public override int? getId() {
@ -32,7 +37,7 @@ namespace FLocal.Common.actions {
Config.instance.mainConnection.lockRow(transaction, this.tableSpec, this.id.ToString()); Config.instance.mainConnection.lockRow(transaction, this.tableSpec, this.id.ToString());
} else { } else {
Config.instance.mainConnection.lockTable(transaction, this.tableSpec); Config.instance.mainConnection.lockTable(transaction, this.tableSpec);
ids = Config.instance.mainConnection.LoadIdsByConditions(this.tableSpec, this.condition, Diapasone.unlimited, new JoinSpec[0]); ids = Config.instance.mainConnection.LoadIdsByConditions(transaction, this.tableSpec, this.condition, Diapasone.unlimited, new JoinSpec[0], new SortSpec[0], false);
if(ids.Count > 1) { if(ids.Count > 1) {
throw new CriticalException("Not unique"); throw new CriticalException("Not unique");
} else if(ids.Count == 1) { } else if(ids.Count == 1) {
@ -44,11 +49,12 @@ namespace FLocal.Common.actions {
} }
protected override void doApply(Transaction transaction) { protected override void doApply(Transaction transaction) {
if(this.id.HasValue) {
Dictionary<string, string> row = Config.instance.mainConnection.LoadByIds(transaction, this.tableSpec, new List<string>() { this.id.ToString() })[0];
Dictionary<string, string> processedData = new Dictionary<string,string>(); Dictionary<string, string> processedData = new Dictionary<string,string>();
foreach(KeyValuePair<string, AbstractFieldValue> kvp in this.data) { foreach(KeyValuePair<string, AbstractFieldValue> kvp in this.dataToUpdate) {
processedData[kvp.Key] = kvp.Value.getStringRepresentation(); processedData[kvp.Key] = kvp.Value.getStringRepresentation(row[kvp.Key]);
} }
if(this.id.HasValue) {
Config.instance.mainConnection.update( Config.instance.mainConnection.update(
transaction, transaction,
this.tableSpec, this.tableSpec,
@ -56,6 +62,10 @@ namespace FLocal.Common.actions {
processedData processedData
); );
} else { } else {
Dictionary<string, string> processedData = new Dictionary<string,string>();
foreach(KeyValuePair<string, AbstractFieldValue> kvp in this.dataToInsert) {
processedData[kvp.Key] = kvp.Value.getStringRepresentation();
}
Config.instance.mainConnection.insert( Config.instance.mainConnection.insert(
transaction, transaction,
this.tableSpec, this.tableSpec,

@ -8,10 +8,12 @@ namespace FLocal.Common.actions {
public class UpdateChange : AbstractChange { public class UpdateChange : AbstractChange {
private readonly int id; private readonly int id;
private Dictionary<string, AbstractFieldValue> data;
public UpdateChange(ISqlObjectTableSpec tableSpec, Dictionary<string, AbstractFieldValue> data, int id) public UpdateChange(ISqlObjectTableSpec tableSpec, Dictionary<string, AbstractFieldValue> data, int id)
: base(tableSpec, data) { : base(tableSpec, from kvp in data select kvp.Value) {
this.id = id; this.id = id;
this.data = data;
} }
public override int? getId() { public override int? getId() {

@ -142,7 +142,7 @@ namespace FLocal.Common.dataobjects {
); );
} }
public XElement exportToXmlWithoutThread(UserContext context, bool includeParentPost) { public XElement exportToXmlWithoutThread(UserContext context, bool includeParentPost, params XElement[] additional) {
XElement result = new XElement("post", XElement result = new XElement("post",
new XElement("id", this.id), new XElement("id", this.id),
new XElement("poster", this.poster.exportToXmlForViewing(context)), new XElement("poster", this.poster.exportToXmlForViewing(context)),
@ -160,6 +160,9 @@ namespace FLocal.Common.dataobjects {
result.Add(new XElement("parentPost", this.parentPost.exportToXmlWithoutThread(context, false))); result.Add(new XElement("parentPost", this.parentPost.exportToXmlWithoutThread(context, false)));
} }
} }
if(additional.Length > 0) {
result.Add(additional);
}
return result; return result;
} }

@ -30,6 +30,18 @@ namespace FLocal.Common.dataobjects {
public void refreshSqlObject(int id) { Refresh(id); } public void refreshSqlObject(int id) { Refresh(id); }
} }
public class ReadMarkerTableSpec : ISqlObjectTableSpec {
public const string TABLE = "Threads_ReadMarkers";
public const string FIELD_ID = "Id";
public const string FIELD_THREADID = "ThreadId";
public const string FIELD_ACCOUNTID = "AccountId";
public const string FIELD_POSTID = "PostId";
public static readonly ReadMarkerTableSpec instance = new ReadMarkerTableSpec();
public string name { get { return TABLE; } }
public string idName { get { return FIELD_ID; } }
public void refreshSqlObject(int id) { }
}
protected override ISqlObjectTableSpec table { get { return TableSpec.instance; } } protected override ISqlObjectTableSpec table { get { return TableSpec.instance; } }
private int _boardId; private int _boardId;
@ -152,7 +164,7 @@ namespace FLocal.Common.dataobjects {
); );
} }
public XElement exportToXml(UserContext context, bool includeFirstPost) { public XElement exportToXml(UserContext context, bool includeFirstPost, params XElement[] additional) {
XElement result = new XElement("thread", XElement result = new XElement("thread",
new XElement("id", this.id), new XElement("id", this.id),
new XElement("firstPostId", this.firstPostId), new XElement("firstPostId", this.firstPostId),
@ -164,13 +176,15 @@ namespace FLocal.Common.dataobjects {
new XElement("isLocked", this.isLocked), new XElement("isLocked", this.isLocked),
new XElement("totalPosts", this.totalPosts), new XElement("totalPosts", this.totalPosts),
new XElement("totalViews", this.totalViews), new XElement("totalViews", this.totalViews),
new XElement("hasNewPosts", this.hasNewPosts()),
new XElement("bodyShort", this.firstPost.bodyShort), new XElement("bodyShort", this.firstPost.bodyShort),
context.formatTotalPosts(this.totalPosts) context.formatTotalPosts(this.totalPosts)
); );
if(includeFirstPost) { if(includeFirstPost) {
result.Add(new XElement("firstPost", this.firstPost.exportToXmlWithoutThread(context, false))); result.Add(new XElement("firstPost", this.firstPost.exportToXmlWithoutThread(context, false)));
} }
if(additional.Length > 0) {
result.Add(additional);
}
return result; return result;
} }
@ -210,6 +224,131 @@ namespace FLocal.Common.dataobjects {
}); });
} }
private Post getLastRead(Account account) {
List<string> stringIds = Config.instance.mainConnection.LoadIdsByConditions(
ReadMarkerTableSpec.instance,
new ComplexCondition(
ConditionsJoinType.AND,
new ComparisonCondition(
ReadMarkerTableSpec.instance.getColumnSpec(ReadMarkerTableSpec.FIELD_THREADID),
ComparisonType.EQUAL,
this.id.ToString()
),
new ComparisonCondition(
ReadMarkerTableSpec.instance.getColumnSpec(ReadMarkerTableSpec.FIELD_ACCOUNTID),
ComparisonType.EQUAL,
account.id.ToString()
)
),
Diapasone.unlimited
);
if(stringIds.Count > 1) {
throw new CriticalException("more than one row");
}
if(stringIds.Count < 1) {
return null;
}
Dictionary<string, string> data = Config.instance.mainConnection.LoadById(ReadMarkerTableSpec.instance, stringIds[0]);
if((data[ReadMarkerTableSpec.FIELD_POSTID] == "") || (data[ReadMarkerTableSpec.FIELD_POSTID] == null)) {
return null;
}
return Post.LoadById(int.Parse(data[ReadMarkerTableSpec.FIELD_POSTID]));
}
public int getLastReadId(Session session) {
if(session == null) {
return 0;
}
Post post = this.getLastRead(session.account);
if(post == null) {
return 0;
}
return post.id;
}
public void markAsRead(Account account, Post minPost, Post maxPost) {
ChangeSetUtil.ApplyChanges(new AbstractChange[] {
new InsertOrUpdateChange(
ReadMarkerTableSpec.instance,
new Dictionary<string,AbstractFieldValue> {
{
ReadMarkerTableSpec.FIELD_THREADID,
new ScalarFieldValue(this.id.ToString())
},
{
ReadMarkerTableSpec.FIELD_ACCOUNTID,
new ScalarFieldValue(account.id.ToString())
},
{
ReadMarkerTableSpec.FIELD_POSTID,
new ScalarFieldValue(
(minPost.id < this.firstPostId)
?
maxPost.id.ToString()
:
null
) },
},
new Dictionary<string,AbstractFieldValue> {
{
ReadMarkerTableSpec.FIELD_POSTID,
new IncrementFieldValue(
s => {
if((s == null) || (s == "")) {
s = "0"; //workaround
}
if(maxPost.id < int.Parse(s)) {
return (s == "0") ? null : s; //if some newer posts were already read
}
long count = Config.instance.mainConnection.GetCountByConditions(
Post.TableSpec.instance,
new ComplexCondition(
ConditionsJoinType.AND,
new ComparisonCondition(
Post.TableSpec.instance.getColumnSpec(Post.TableSpec.FIELD_THREADID),
ComparisonType.EQUAL,
this.id.ToString()
),
new ComparisonCondition(
Post.TableSpec.instance.getIdSpec(),
ComparisonType.GREATERTHAN,
s
),
new ComparisonCondition(
Post.TableSpec.instance.getIdSpec(),
ComparisonType.LESSTHAN,
minPost.id.ToString()
)
),
new JoinSpec[0]
);
if(count > 0) {
return (s == "0") ? null : s; //if there are some unread posts earlier than minPost
} else {
return maxPost.id.ToString();
}
}
)
}
},
new ComplexCondition(
ConditionsJoinType.AND,
new ComparisonCondition(
ReadMarkerTableSpec.instance.getColumnSpec(ReadMarkerTableSpec.FIELD_THREADID),
ComparisonType.EQUAL,
this.id.ToString()
),
new ComparisonCondition(
ReadMarkerTableSpec.instance.getColumnSpec(ReadMarkerTableSpec.FIELD_ACCOUNTID),
ComparisonType.EQUAL,
account.id.ToString()
)
)
)
});
}
} }
} }

@ -20,6 +20,8 @@ namespace FLocal.Core.DB {
List<Dictionary<string, string>> LoadByIds(Transaction transaction, ITableSpec table, List<string> ids); List<Dictionary<string, string>> LoadByIds(Transaction transaction, ITableSpec table, List<string> ids);
List<string> LoadIdsByConditions(Transaction transaction, ITableSpec table, conditions.AbstractCondition conditions, Diapasone diapasone, JoinSpec[] joins, SortSpec[] sorts, bool allowHugeLists);
void update(Transaction transaction, ITableSpec table, string id, Dictionary<string, string> data); void update(Transaction transaction, ITableSpec table, string id, Dictionary<string, string> data);
string insert(Transaction transaction, ITableSpec table, Dictionary<string, string> data); string insert(Transaction transaction, ITableSpec table, Dictionary<string, string> data);

@ -8,9 +8,9 @@ namespace FLocal.Core.DB.conditions {
public readonly ConditionsJoinType type; public readonly ConditionsJoinType type;
public readonly List<NotEmptyCondition> innerConditions; public readonly NotEmptyCondition[] innerConditions;
public ComplexCondition(ConditionsJoinType type, List<NotEmptyCondition> innerConditions) { public ComplexCondition(ConditionsJoinType type, params NotEmptyCondition[] innerConditions) {
this.type = type; this.type = type;
this.innerConditions = innerConditions; this.innerConditions = innerConditions;
} }

@ -26,7 +26,7 @@ namespace FLocal.IISHandler.handlers {
new XElement("currentLocation", board.exportToXmlSimpleWithParent(context)), new XElement("currentLocation", board.exportToXmlSimpleWithParent(context)),
new XElement("boards", from subBoard in board.subBoards select subBoard.exportToXml(context, true)), new XElement("boards", from subBoard in board.subBoards select subBoard.exportToXml(context, true)),
new XElement("threads", new XElement("threads",
(from thread in threads select thread.exportToXml(context, false)).addNumbers(), (from thread in threads select thread.exportToXml(context, false, new XElement("afterLastRead", thread.getLastReadId(context.session) + 1))).addNumbers(),
pageOuter.exportToXml(1, 5, 1) pageOuter.exportToXml(1, 5, 1)
) )
}; };

@ -4,6 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Web; using System.Web;
using System.Xml.Linq; using System.Xml.Linq;
using FLocal.Core;
using FLocal.Common; using FLocal.Common;
using FLocal.Common.dataobjects; using FLocal.Common.dataobjects;
@ -19,10 +20,17 @@ namespace FLocal.IISHandler.handlers {
override protected XElement[] getSpecificData(WebContext context) { override protected XElement[] getSpecificData(WebContext context) {
Post post = Post.LoadById(int.Parse(context.requestParts[1])); Post post = Post.LoadById(int.Parse(context.requestParts[1]));
int lastReadId = post.thread.getLastReadId(context.session);
post.thread.incrementViewsCounter(); post.thread.incrementViewsCounter();
if(context.session != null) {
post.thread.markAsRead(context.session.account, post, post);
}
return new XElement[] { return new XElement[] {
new XElement("currentLocation", post.exportToXmlSimpleWithParent(context)), new XElement("currentLocation", post.exportToXmlSimpleWithParent(context)),
new XElement("posts", post.exportToXmlWithoutThread(context, true)) new XElement("posts", post.exportToXmlWithoutThread(context, true, new XElement("isUnread", (post.id > lastReadId).ToPlainString())))
}; };
} }

@ -32,7 +32,6 @@ namespace FLocal.IISHandler.handlers {
Post.TableSpec.instance, Post.TableSpec.instance,
new ComplexCondition( new ComplexCondition(
ConditionsJoinType.AND, ConditionsJoinType.AND,
new List<NotEmptyCondition> {
new ComparisonCondition( new ComparisonCondition(
Post.TableSpec.instance.getColumnSpec(Post.TableSpec.FIELD_THREADID), Post.TableSpec.instance.getColumnSpec(Post.TableSpec.FIELD_THREADID),
ComparisonType.EQUAL, ComparisonType.EQUAL,
@ -42,8 +41,7 @@ namespace FLocal.IISHandler.handlers {
Post.TableSpec.instance.getIdSpec(), Post.TableSpec.instance.getIdSpec(),
ComparisonType.LESSTHAN, ComparisonType.LESSTHAN,
int.Parse(context.requestParts[2].PHPSubstring(1)).ToString() int.Parse(context.requestParts[2].PHPSubstring(1)).ToString()
), )
}
), ),
new JoinSpec[0] new JoinSpec[0]
) )
@ -52,11 +50,22 @@ namespace FLocal.IISHandler.handlers {
2 2
); );
IEnumerable<Post> posts = thread.getPosts(pageOuter, context); IEnumerable<Post> posts = thread.getPosts(pageOuter, context);
int lastReadId = thread.getLastReadId(context.session);
thread.incrementViewsCounter(); thread.incrementViewsCounter();
if((context.session != null) && (posts.Count() > 0)) {
thread.markAsRead(
context.session.account,
(from post in posts orderby post.id ascending select post).First(),
(from post in posts orderby post.id descending select post).First()
);
}
return new XElement[] { return new XElement[] {
new XElement("currentLocation", thread.exportToXmlSimpleWithParent(context)), new XElement("currentLocation", thread.exportToXmlSimpleWithParent(context)),
new XElement("posts", new XElement("posts",
from post in posts select post.exportToXmlWithoutThread(context, true), from post in posts select post.exportToXmlWithoutThread(context, true, new XElement("isUnread", (post.id > lastReadId).ToPlainString())),
pageOuter.exportToXml(2, 5, 2) pageOuter.exportToXml(2, 5, 2)
) )
}; };

@ -32,6 +32,7 @@ namespace FLocal.MySQLConnector {
} }
private List<Dictionary<string, string>> _LoadByIds(DbCommand command, ITableSpec table, List<string> ids, bool forUpdate) { private List<Dictionary<string, string>> _LoadByIds(DbCommand command, ITableSpec table, List<string> ids, bool forUpdate) {
lock(this) {
command.CommandType = System.Data.CommandType.Text; command.CommandType = System.Data.CommandType.Text;
ParamsHolder paramsHolder = new ParamsHolder(); ParamsHolder paramsHolder = new ParamsHolder();
@ -73,6 +74,7 @@ namespace FLocal.MySQLConnector {
} }
return result; return result;
} }
}
public List<Dictionary<string, string>> LoadByIds(ITableSpec table, List<string> ids) { public List<Dictionary<string, string>> LoadByIds(ITableSpec table, List<string> ids) {
lock(this) { lock(this) {
@ -82,10 +84,7 @@ namespace FLocal.MySQLConnector {
} }
} }
public List<string> LoadIdsByConditions(ITableSpec table, FLocal.Core.DB.conditions.AbstractCondition conditions, Diapasone diapasone, JoinSpec[] joins, SortSpec[] sorts, bool allowHugeLists) { private List<string> _LoadIdsByConditions(DbCommand command, ITableSpec table, FLocal.Core.DB.conditions.AbstractCondition conditions, Diapasone diapasone, JoinSpec[] joins, SortSpec[] sorts, bool allowHugeLists) {
lock(this) {
using(DbCommand command = this.connection.CreateCommand()) {
command.CommandType = System.Data.CommandType.Text; command.CommandType = System.Data.CommandType.Text;
var conditionsCompiled = ConditionCompiler.Compile(conditions, this.traits); var conditionsCompiled = ConditionCompiler.Compile(conditions, this.traits);
@ -150,6 +149,12 @@ namespace FLocal.MySQLConnector {
return result; return result;
} }
} }
public List<string> LoadIdsByConditions(ITableSpec table, FLocal.Core.DB.conditions.AbstractCondition conditions, Diapasone diapasone, JoinSpec[] joins, SortSpec[] sorts, bool allowHugeLists) {
lock(this) {
using(DbCommand command = this.connection.CreateCommand()) {
return this._LoadIdsByConditions(command, table, conditions, diapasone, joins, sorts, allowHugeLists);
}
} }
} }
@ -231,6 +236,16 @@ namespace FLocal.MySQLConnector {
} }
} }
public List<string> LoadIdsByConditions(FLocal.Core.DB.Transaction _transaction, ITableSpec table, FLocal.Core.DB.conditions.AbstractCondition conditions, Diapasone diapasone, JoinSpec[] joins, SortSpec[] sorts, bool allowHugeLists) {
Transaction transaction = (Transaction)_transaction;
lock(this) {
using(DbCommand command = transaction.sqlconnection.CreateCommand()) {
command.Transaction = transaction.sqltransaction;
return this._LoadIdsByConditions(command, table, conditions, diapasone, joins, sorts, allowHugeLists);
}
}
}
public void update(FLocal.Core.DB.Transaction _transaction, ITableSpec table, string id, Dictionary<string, string> data) { public void update(FLocal.Core.DB.Transaction _transaction, ITableSpec table, string id, Dictionary<string, string> data) {
Transaction transaction = (Transaction)_transaction; Transaction transaction = (Transaction)_transaction;
lock(transaction) { lock(transaction) {

@ -17,10 +17,21 @@
<td align="left" width="65%" valign="top"> <td align="left" width="65%" valign="top">
<a target="_blank" class="separate"> <a target="_blank" class="separate">
<xsl:attribute name="href">/Post/<xsl:value-of select="id"/>/</xsl:attribute> <xsl:attribute name="href">/Post/<xsl:value-of select="id"/>/</xsl:attribute>
<img border="0" src="/static/images/book-notread.gif" alt="" style="vertical-align: text-bottom" /> <img border="0" alt="" style="vertical-align: text-bottom">
<xsl:choose>
<xsl:when test="isUnread='true'">
<xsl:attribute name="src">/static/images/book-notread.gif</xsl:attribute>
</xsl:when>
<xsl:otherwise>
<xsl:attribute name="src">/static/images/book-read.gif</xsl:attribute>
</xsl:otherwise>
</xsl:choose>
</img>
</a> </a>
<b class="separate"><xsl:value-of select="title"/></b> <b class="separate"><xsl:value-of select="title"/></b>
<xsl:if test="isUnread='true'">
<img alt="new" src="/static/images/new.gif" /> <img alt="new" src="/static/images/new.gif" />
</xsl:if>
<xsl:if test="parentPost/post"> <xsl:if test="parentPost/post">
<font class="small separate"> <font class="small separate">
<xsl:text>[</xsl:text> <xsl:text>[</xsl:text>

@ -15,7 +15,7 @@
<xsl:attribute name="title"><xsl:value-of select="bodyShort"/></xsl:attribute> <xsl:attribute name="title"><xsl:value-of select="bodyShort"/></xsl:attribute>
<img alt="*" hspace="5" style="vertical-align: text-bottom"> <img alt="*" hspace="5" style="vertical-align: text-bottom">
<xsl:choose> <xsl:choose>
<xsl:when test="hasNewPosts='true'"> <xsl:when test="afterLastRead&lt;=lastPostId">
<xsl:attribute name="src">/static/images/book-notread.gif</xsl:attribute> <xsl:attribute name="src">/static/images/book-notread.gif</xsl:attribute>
</xsl:when> </xsl:when>
<xsl:otherwise> <xsl:otherwise>
@ -24,7 +24,15 @@
</xsl:choose> </xsl:choose>
</img> </img>
<a> <a>
<xsl:attribute name="href">/Thread/<xsl:value-of select="id"/>/</xsl:attribute> <xsl:attribute name="href">
<xsl:text>/Thread/</xsl:text>
<xsl:value-of select="id"/>
<xsl:text>/</xsl:text>
<xsl:if test="afterLastRead&lt;=lastPostId">
<xsl:text>p</xsl:text>
<xsl:value-of select="afterLastRead"/>
</xsl:if>
</xsl:attribute>
<xsl:if test="isAnnouncement='true'"> <xsl:if test="isAnnouncement='true'">
<img src="/static/images/sticky.gif" class="separate" width="16" height="16" alt="" border="0" style="vertical-align: text-bottom;" /> <img src="/static/images/sticky.gif" class="separate" width="16" height="16" alt="" border="0" style="vertical-align: text-bottom;" />
</xsl:if> </xsl:if>

Loading…
Cancel
Save